import json
import os
import pytest
from unittest.mock import MagicMock, patch

from brynq import auto_setup


# ── Fixtures ──────────────────────────────────────────────────────────────


@pytest.fixture
def mock_probe_url():
    """Mock the low-level URL probe used by detect_ollama."""
    with patch("brynq.auto_setup._probe_url") as mock:
        yield mock


@pytest.fixture
def mock_http_json():
    """Mock the HTTP JSON helper used to query /api/tags."""
    with patch("brynq.auto_setup._http_json") as mock:
        yield mock


@pytest.fixture
def mock_shutil_which():
    """Mock shutil.which for binary detection."""
    with patch("shutil.which") as mock:
        yield mock


@pytest.fixture
def agents_dir(tmp_path):
    """Redirect spawner state to a temp directory."""
    with patch("brynq.auto_setup.AGENTS_DIR", tmp_path / "agents"), \
         patch("brynq.auto_setup.spawn_agent") as mock_spawn, \
         patch("brynq.auto_setup._post_to_channel") as mock_post, \
         patch("brynq.auto_setup.list_agents") as mock_list:
        (tmp_path / "agents").mkdir()
        mock_spawn.return_value = {"name": "test", "pid": 9999, "status": "running"}
        mock_list.return_value = []
        yield {
            "tmp_path": tmp_path,
            "spawn_agent": mock_spawn,
            "post_to_channel": mock_post,
            "list_agents": mock_list,
        }


# ── detect_ollama ─────────────────────────────────────────────────────────


class TestDetectOllama:
    def test_running_with_models(self, mock_probe_url, mock_http_json):
        """Ollama is running and has models loaded."""
        mock_probe_url.return_value = True
        mock_http_json.return_value = {
            "models": [
                {"name": "llama3:latest"},
                {"name": "mistral:7b"},
            ]
        }

        result = auto_setup.detect_ollama()

        assert result["status"] == "running"
        assert result["models"] == ["llama3:latest", "mistral:7b"]
        assert result["url"] == "http://localhost:11434"
        assert result["error"] is None

    def test_running_no_models(self, mock_probe_url, mock_http_json):
        """Ollama is running but no models are pulled."""
        mock_probe_url.return_value = True
        mock_http_json.return_value = {"models": []}

        result = auto_setup.detect_ollama()

        assert result["status"] == "running"
        assert result["models"] == []

    def test_running_but_tags_fails(self, mock_probe_url, mock_http_json):
        """Ollama server responds but /api/tags returns an error."""
        mock_probe_url.return_value = True
        mock_http_json.side_effect = RuntimeError("500 Internal Server Error")

        result = auto_setup.detect_ollama()

        assert result["status"] == "running"
        assert result["models"] == []
        assert result["error"] is not None

    def test_not_running_but_installed(self, mock_probe_url, mock_http_json, mock_shutil_which):
        """Ollama binary is on PATH but the server is not running."""
        mock_probe_url.return_value = False
        mock_shutil_which.return_value = "/usr/local/bin/ollama"

        result = auto_setup.detect_ollama()

        assert result["status"] == "not_running"
        assert "not running" in result["error"]
        assert result["models"] == []

    def test_not_installed(self, mock_probe_url, mock_http_json, mock_shutil_which):
        """Ollama is not installed at all."""
        mock_probe_url.return_value = False
        mock_shutil_which.return_value = None

        result = auto_setup.detect_ollama()

        assert result["status"] == "not_installed"
        assert "not installed" in result["error"]
        assert result["models"] == []


# ── get_recommended_models ────────────────────────────────────────────────


class TestGetRecommendedModels:
    def test_priority_order_llama_first(self):
        """llama3 should be recommended first when available."""
        models = ["mistral:7b", "llama3:latest", "nomic-embed-text"]
        result = auto_setup.get_recommended_models(models)

        assert len(result) >= 1
        assert result[0]["name"] == "llama3:latest"

    def test_priority_mistral_over_phi(self):
        """mistral should rank above phi3."""
        models = ["phi3:latest", "mistral:7b"]
        result = auto_setup.get_recommended_models(models)

        assert result[0]["name"] == "mistral:7b"
        assert result[1]["name"] == "phi3:latest"

    def test_max_recommendations_respected(self):
        """Should not return more than max_recommendations."""
        models = ["llama3:latest", "mistral:7b", "phi3:latest", "gemma:7b", "codellama:7b"]
        result = auto_setup.get_recommended_models(models, max_recommendations=2)

        assert len(result) == 2

    def test_empty_models_returns_empty(self):
        """No available models should return an empty list."""
        result = auto_setup.get_recommended_models([])
        assert result == []

    def test_unknown_models_still_returned(self):
        """Models not in the catalog should still be recommended as fallback."""
        models = ["custom-model:v1"]
        result = auto_setup.get_recommended_models(models)

        assert len(result) == 1
        assert result[0]["name"] == "custom-model:v1"
        assert "local model" in result[0]["description"]

    def test_descriptions_from_catalog(self):
        """Known models should get their catalog descriptions."""
        models = ["llama3:latest"]
        result = auto_setup.get_recommended_models(models)

        assert "Llama 3" in result[0]["description"]

    def test_no_duplicate_families(self):
        """Should not recommend two models from the same family."""
        models = ["llama3:latest", "llama3:8b", "mistral:7b"]
        result = auto_setup.get_recommended_models(models, max_recommendations=3)

        names = [r["name"] for r in result]
        # Should have llama3:latest (first match), mistral:7b, then llama3:8b as fallback
        assert names[0] == "llama3:latest"
        assert names[1] == "mistral:7b"

    def test_codellama_matched(self):
        """codellama should be recognized as its own family."""
        models = ["codellama:7b"]
        result = auto_setup.get_recommended_models(models)

        assert result[0]["name"] == "codellama:7b"
        assert "Code Llama" in result[0]["description"]


# ── spawn_welcome_agents ──────────────────────────────────────────────────


class TestSpawnWelcomeAgents:
    def test_creates_welcome_channel(self, agents_dir):
        """Should create a channel named #welcome-{user_id}."""
        models = [{"name": "llama3:latest", "description": "Llama 3"}]
        result = auto_setup.spawn_welcome_agents("user-42", models)

        assert result["channel"] == "welcome-user-42"

    def test_spawns_agents_for_each_model(self, agents_dir):
        """Should spawn one agent per model (up to 2)."""
        models = [
            {"name": "llama3:latest", "description": "Llama 3"},
            {"name": "mistral:7b", "description": "Mistral"},
        ]
        result = auto_setup.spawn_welcome_agents("user-42", models)

        assert len(result["agents"]) == 2
        assert agents_dir["spawn_agent"].call_count == 2

    def test_limits_to_two_agents(self, agents_dir):
        """Even with 3+ models, only spawn 2 welcome agents."""
        models = [
            {"name": "llama3:latest", "description": ""},
            {"name": "mistral:7b", "description": ""},
            {"name": "phi3:latest", "description": ""},
        ]
        result = auto_setup.spawn_welcome_agents("user-42", models)

        assert len(result["agents"]) == 2

    def test_posts_three_welcome_messages(self, agents_dir):
        """With 2 agents, should post 3 staggered messages."""
        models = [
            {"name": "llama3:latest", "description": "Llama 3"},
            {"name": "mistral:7b", "description": "Mistral"},
        ]
        result = auto_setup.spawn_welcome_agents("user-42", models)

        assert result["messages_posted"] == 3
        assert agents_dir["post_to_channel"].call_count == 3

    def test_welcome_messages_content(self, agents_dir):
        """Verify the content of the staggered welcome messages."""
        models = [
            {"name": "llama3:latest", "description": "Llama 3"},
            {"name": "mistral:7b", "description": "Mistral"},
        ]
        auto_setup.spawn_welcome_agents("user-42", models)

        calls = agents_dir["post_to_channel"].call_args_list
        # Message 1: agent 1 intro
        assert "llama3" in calls[0][0][1].lower()
        assert "your machine" in calls[0][0][1].lower()
        # Message 2: agent 2 intro
        assert "mistral" in calls[1][0][1].lower()
        # Message 3: upsell to cloud
        assert "Claude" in calls[2][0][1] or "Gemini" in calls[2][0][1]
        assert "Settings" in calls[2][0][1]

    def test_single_model_posts_two_messages(self, agents_dir):
        """With only 1 model, should post 2 messages (intro + upsell)."""
        models = [{"name": "llama3:latest", "description": "Llama 3"}]
        result = auto_setup.spawn_welcome_agents("user-42", models)

        assert result["messages_posted"] == 2
        assert agents_dir["post_to_channel"].call_count == 2

    def test_raises_on_empty_models(self, agents_dir):
        """Should raise ValueError if no models provided."""
        with pytest.raises(ValueError, match="(?i)at least one model"):
            auto_setup.spawn_welcome_agents("user-42", [])

    def test_handles_spawn_failure_gracefully(self, agents_dir):
        """If spawn_agent raises, the failure is recorded but does not crash."""
        agents_dir["spawn_agent"].side_effect = RuntimeError("port conflict")
        models = [{"name": "llama3:latest", "description": "Llama 3"}]

        result = auto_setup.spawn_welcome_agents("user-42", models)

        assert result["agents"][0]["status"] == "failed"
        # No welcome messages posted since no agent is running
        assert result["messages_posted"] == 0


# ── check_user_setup_status ──────────────────────────────────────────────


class TestCheckUserSetupStatus:
    def test_needs_onboarding_when_nothing_configured(self, agents_dir):
        """Fresh user with no Ollama, no keys, no agents needs onboarding."""
        with patch("brynq.auto_setup.detect_ollama") as mock_detect, \
             patch.dict(os.environ, {
                 "ANTHROPIC_API_KEY": "",
                 "GEMINI_API_KEY": "",
             }, clear=False):
            mock_detect.return_value = {
                "status": "not_installed",
                "models": [],
                "url": "",
                "error": None,
            }
            agents_dir["list_agents"].return_value = []

            result = auto_setup.check_user_setup_status("user-new")

            assert result["needs_onboarding"] is True
            assert result["ollama_running"] is False
            assert result["local_models"] == []
            assert result["agents_spawned"] == []

    def test_no_onboarding_when_agents_exist(self, agents_dir):
        """User with running welcome agents does not need onboarding."""
        with patch("brynq.auto_setup.detect_ollama") as mock_detect:
            mock_detect.return_value = {
                "status": "running",
                "models": ["llama3:latest"],
                "url": "http://localhost:11434",
                "error": None,
            }
            agents_dir["list_agents"].return_value = [
                {"name": "welcome-user-42-llama3", "status": "running", "pid": 1234},
            ]

            result = auto_setup.check_user_setup_status("user-42")

            assert result["needs_onboarding"] is False
            assert result["ollama_running"] is True
            assert len(result["agents_spawned"]) == 1

    def test_no_onboarding_when_cloud_key_set(self, agents_dir):
        """User with a cloud API key does not need onboarding."""
        with patch("brynq.auto_setup.detect_ollama") as mock_detect, \
             patch.dict(os.environ, {"ANTHROPIC_API_KEY": "sk-test-key"}):
            mock_detect.return_value = {
                "status": "not_installed",
                "models": [],
                "url": "",
                "error": None,
            }
            agents_dir["list_agents"].return_value = []

            result = auto_setup.check_user_setup_status("user-cloud")

            assert result["needs_onboarding"] is False
            assert result["cloud_keys"]["anthropic"] is True

    def test_cloud_keys_detection(self, agents_dir):
        """Correctly reports which cloud keys are set."""
        with patch("brynq.auto_setup.detect_ollama") as mock_detect, \
             patch.dict(os.environ, {
                 "ANTHROPIC_API_KEY": "",
                 "GEMINI_API_KEY": "gm-test-key",
             }, clear=False):
            mock_detect.return_value = {
                "status": "not_installed",
                "models": [],
                "url": "",
                "error": None,
            }
            agents_dir["list_agents"].return_value = []

            result = auto_setup.check_user_setup_status("user-x")

            assert result["cloud_keys"]["anthropic"] is False
            assert result["cloud_keys"]["gemini"] is True

    def test_filters_agents_by_user_id(self, agents_dir):
        """Only agents matching the user_id prefix are counted."""
        with patch("brynq.auto_setup.detect_ollama") as mock_detect:
            mock_detect.return_value = {
                "status": "running",
                "models": ["llama3:latest"],
                "url": "http://localhost:11434",
                "error": None,
            }
            agents_dir["list_agents"].return_value = [
                {"name": "welcome-user-42-llama3", "status": "running", "pid": 1},
                {"name": "welcome-user-99-mistral", "status": "running", "pid": 2},
                {"name": "some-other-agent", "status": "running", "pid": 3},
            ]

            result = auto_setup.check_user_setup_status("user-42")

            assert len(result["agents_spawned"]) == 1
            assert result["agents_spawned"][0]["name"] == "welcome-user-42-llama3"
