"""Test CLI infrastructure: exit codes, config, completion, shell session,
serve helpers, streaming, and settings."""

import asyncio
import json
from pathlib import Path
from unittest.mock import patch, MagicMock, AsyncMock

import pytest
from click.testing import CliRunner

from mcpbundles.cli import cli
from mcpbundles.exit_codes import SUCCESS, USER_ERROR, TOOL_ERROR, CONNECTION_ERROR, INTERNAL_ERROR


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


# ── Exit codes ─────────────────────────────────────────────────────

class TestExitCodes:
    def test_constants(self):
        assert SUCCESS == 0
        assert USER_ERROR == 1
        assert TOOL_ERROR == 2
        assert CONNECTION_ERROR == 3
        assert INTERNAL_ERROR == 4

    def test_tools_auth_error_exits_1(self, runner):
        with patch("mcpbundles.config.load_token", return_value=None), \
             patch("mcpbundles.connections.ConnectionManager.list_connections", return_value=[]), \
             patch("mcpbundles.connections.ConnectionManager.get_default", return_value=None):
            r = runner.invoke(cli, ["tools"])
            assert r.exit_code == USER_ERROR

    def test_call_no_auth_exits_1(self, runner):
        with patch("mcpbundles.config.load_token", return_value=None), \
             patch("mcpbundles.connections.ConnectionManager.list_connections", return_value=[]), \
             patch("mcpbundles.connections.ConnectionManager.get_default", return_value=None):
            r = runner.invoke(cli, ["call", "some-tool", "k=v"])
            assert r.exit_code == USER_ERROR

    def test_exec_no_code_exits_1(self, runner):
        with patch("mcpbundles.config.load_token", return_value={"access_token": "mb_x", "auth_type": "api_key"}):
            r = runner.invoke(cli, ["exec"])
            assert r.exit_code == USER_ERROR
            assert "Provide code" in r.output

    def test_exec_file_not_found_exits_1(self, runner):
        with patch("mcpbundles.config.load_token", return_value={"access_token": "mb_x", "auth_type": "api_key"}):
            r = runner.invoke(cli, ["exec", "-f", "/nonexistent/file.py"])
            assert r.exit_code == USER_ERROR
            assert "File not found" in r.output


# ── Config reset ───────────────────────────────────────────────────

class TestConfigReset:
    def test_reset_removes_file(self, runner, tmp_path, monkeypatch):
        config_file = tmp_path / "config.yaml"
        config_file.write_text("server: test\n")
        monkeypatch.setattr("mcpbundles.commands.config_cmd.CONFIG_FILE", config_file)

        r = runner.invoke(cli, ["config", "reset"])
        assert r.exit_code == 0
        assert not config_file.exists()

    def test_reset_no_file_ok(self, runner, tmp_path, monkeypatch):
        config_file = tmp_path / "config.yaml"
        monkeypatch.setattr("mcpbundles.commands.config_cmd.CONFIG_FILE", config_file)

        r = runner.invoke(cli, ["config", "reset"])
        assert r.exit_code == 0


# ── Install completion ─────────────────────────────────────────────

class TestInstallCompletion:
    def test_invalid_shell_rejected(self, runner):
        r = runner.invoke(cli, ["install-completion", "powershell"])
        assert r.exit_code == 2

    def test_bash_creates_files(self, runner, tmp_path):
        fake_home = tmp_path / "home"
        fake_home.mkdir()

        with patch.object(Path, "home", return_value=fake_home):
            import importlib
            import mcpbundles.commands.install_completion as ic
            importlib.reload(ic)

            r = runner.invoke(ic.install_completion, ["bash"])
            assert r.exit_code == 0

            bashrc = fake_home / ".bashrc"
            assert bashrc.exists()
            assert "mcpbundles shell completion" in bashrc.read_text()

            script = fake_home / ".mcpbundles" / "completions" / "mcpbundles.bash"
            assert script.exists()
            assert len(script.read_text()) > 50

    def test_bash_idempotent(self, runner, tmp_path):
        fake_home = tmp_path / "home"
        fake_home.mkdir()

        with patch.object(Path, "home", return_value=fake_home):
            import importlib
            import mcpbundles.commands.install_completion as ic
            importlib.reload(ic)

            runner.invoke(ic.install_completion, ["bash"])
            r = runner.invoke(ic.install_completion, ["bash"])
            assert r.exit_code == 0
            assert "already installed" in r.output.lower()

    def test_fish_writes_to_completions_dir(self, runner, tmp_path):
        fake_home = tmp_path / "home"
        fake_home.mkdir()

        with patch.object(Path, "home", return_value=fake_home):
            import importlib
            import mcpbundles.commands.install_completion as ic
            importlib.reload(ic)

            r = runner.invoke(ic.install_completion, ["fish"])
            assert r.exit_code == 0

            fish_file = fake_home / ".config" / "fish" / "completions" / "mcpbundles.fish"
            assert fish_file.exists()

    def test_zsh_creates_files(self, runner, tmp_path):
        fake_home = tmp_path / "home"
        fake_home.mkdir()

        with patch.object(Path, "home", return_value=fake_home):
            import importlib
            import mcpbundles.commands.install_completion as ic
            importlib.reload(ic)

            r = runner.invoke(ic.install_completion, ["zsh"])
            assert r.exit_code == 0

            zshrc = fake_home / ".zshrc"
            assert zshrc.exists()
            assert "mcpbundles shell completion" in zshrc.read_text()


# ── Serve helpers ──────────────────────────────────────────────────

class TestServeHelpers:
    def test_build_auth_headers_api_key(self):
        from mcpbundles.commands.agent import _build_auth_headers
        h = _build_auth_headers("mb_key_123")
        assert h == {"X-API-Key": "mb_key_123"}

    def test_build_auth_headers_bearer(self):
        from mcpbundles.commands.agent import _build_auth_headers
        h = _build_auth_headers("eyJtoken")
        assert h == {"Authorization": "Bearer eyJtoken"}

    def test_upstream_target_dataclass(self):
        from mcpbundles.commands.agent import _UpstreamTarget
        t = _UpstreamTarget(name="hub", url="https://api.mcpbundles.com", endpoint="/hub/", auth="mb_test")
        assert t.name == "hub"
        assert t.url == "https://api.mcpbundles.com"
        assert t.endpoint == "/hub/"
        assert t.auth == "mb_test"

    def test_serve_help_shows_connections(self, runner):
        r = runner.invoke(cli, ["serve", "--help"])
        assert "--connections TEXT" in r.output
        assert "Aggregate multiple" in r.output

    def test_serve_connections_nonexistent_fails(self, runner):
        with patch("mcpbundles.config.load_token", return_value={"access_token": "mb_x", "auth_type": "api_key"}):
            with patch("mcpbundles.connections.ConnectionManager.get", return_value=None):
                r = runner.invoke(cli, ["serve", "--connections", "nope"])
                assert r.exit_code == 1
                assert "not found" in r.output


# ── Shell session ──────────────────────────────────────────────────

class TestShellSession:
    def test_init_state(self):
        from mcpbundles.commands.shell_cmd import _ShellSession, ShellCompleter
        s = _ShellSession(ShellCompleter())
        assert s.conn_name == ""
        assert s.url == ""
        assert s.endpoint == ""
        assert s._session is None

    def test_session_raises_when_disconnected(self):
        from mcpbundles.commands.shell_cmd import _ShellSession, ShellCompleter
        from mcpbundles.mcp_client import ConnectionError
        s = _ShellSession(ShellCompleter())
        with pytest.raises(ConnectionError, match="No active session"):
            _ = s.session

    def test_disconnect_safe_when_not_connected(self):
        from mcpbundles.commands.shell_cmd import _ShellSession, ShellCompleter
        s = _ShellSession(ShellCompleter())
        asyncio.run(s.disconnect())

    def test_connect_cleans_up_on_failure(self):
        from mcpbundles.commands.shell_cmd import _ShellSession, ShellCompleter
        s = _ShellSession(ShellCompleter())
        with pytest.raises(Exception):
            asyncio.run(s.connect("https://api.mcpbundles.com", "/hub/", "test", "fake_auth"))
        assert s._session is None
        assert s._stack is None


# ── Shell completer ────────────────────────────────────────────────

class TestShellCompleter:
    def test_command_completion(self):
        from mcpbundles.commands.shell_cmd import ShellCompleter
        from prompt_toolkit.document import Document
        c = ShellCompleter()

        doc = Document("to", cursor_position=2)
        completions = list(c.get_completions(doc, None))
        names = [comp.text for comp in completions]
        assert "tools" in names

    def test_tool_name_completion(self):
        from mcpbundles.commands.shell_cmd import ShellCompleter
        from prompt_toolkit.document import Document
        c = ShellCompleter()
        c.update_tools(["get-contacts", "get-deals", "search"])

        doc = Document("call get-", cursor_position=9)
        completions = list(c.get_completions(doc, None))
        names = [comp.text for comp in completions]
        assert "get-contacts" in names
        assert "get-deals" in names
        assert "search" not in names

    def test_empty_tools(self):
        from mcpbundles.commands.shell_cmd import ShellCompleter
        from prompt_toolkit.document import Document
        c = ShellCompleter()
        doc = Document("call ", cursor_position=5)
        completions = list(c.get_completions(doc, None))
        assert completions == []


# ── Quiet flag ─────────────────────────────────────────────────────

class TestQuietFlag:
    def test_quiet_flag_registered(self, runner):
        r = runner.invoke(cli, ["--help"])
        assert "-q" in r.output
        assert "--quiet" in r.output

    def test_quiet_sets_module_flag(self, runner):
        import mcpbundles.output as out_mod
        r = runner.invoke(cli, ["-q", "--help"])
        assert r.exit_code == 0


# ── Exec --stream flag ────────────────────────────────────────────

class TestExecStreamFlag:
    def test_stream_flag_in_help(self, runner):
        r = runner.invoke(cli, ["exec", "--help"])
        assert "--stream" in r.output

    def test_stream_no_code_still_errors(self, runner):
        with patch("mcpbundles.config.load_token", return_value={"access_token": "mb_x", "auth_type": "api_key"}):
            r = runner.invoke(cli, ["exec", "--stream"])
            assert r.exit_code == USER_ERROR


# ── Completion cache ───────────────────────────────────────────────

class TestCompletionCache:
    def test_update_and_get(self, tmp_path, monkeypatch):
        cache_file = tmp_path / "cache.json"
        monkeypatch.setattr("mcpbundles.completion.CACHE_FILE", cache_file)
        monkeypatch.setattr("mcpbundles.completion.CONFIG_DIR", tmp_path)

        from mcpbundles.completion import update_cache, get_tool_names, get_resource_uris, get_prompt_names

        update_cache(["tool-a", "tool-b"], ["res://x"], ["prompt-1"])
        assert get_tool_names() == ["tool-a", "tool-b"]
        assert get_resource_uris() == ["res://x"]
        assert get_prompt_names() == ["prompt-1"]

    def test_expired_cache_returns_empty(self, tmp_path, monkeypatch):
        import time
        cache_file = tmp_path / "cache.json"
        monkeypatch.setattr("mcpbundles.completion.CACHE_FILE", cache_file)

        data = {"ts": time.time() - 120, "tools": ["old"], "resources": [], "prompts": []}
        cache_file.write_text(json.dumps(data))

        from mcpbundles.completion import get_tool_names
        assert get_tool_names() == []

    def test_missing_cache_returns_empty(self, tmp_path, monkeypatch):
        monkeypatch.setattr("mcpbundles.completion.CACHE_FILE", tmp_path / "nope.json")
        from mcpbundles.completion import get_tool_names
        assert get_tool_names() == []

    def test_corrupt_cache_returns_empty(self, tmp_path, monkeypatch):
        cache_file = tmp_path / "cache.json"
        cache_file.write_text("not json")
        monkeypatch.setattr("mcpbundles.completion.CACHE_FILE", cache_file)
        from mcpbundles.completion import get_tool_names
        assert get_tool_names() == []


# ── CLI settings ───────────────────────────────────────────────────

class TestCLISettings:
    def test_resolve_defaults(self):
        from mcpbundles.config import CLISettings
        settings = CLISettings.resolve()
        assert "mcpbundles.com" in settings.base_url
        assert hasattr(settings, "base_url")
        assert hasattr(settings, "config_dir")

    def test_resolve_url_override(self):
        from mcpbundles.config import CLISettings
        settings = CLISettings.resolve(url_override="http://localhost:8000")
        assert settings.base_url == "http://localhost:8000"

    def test_is_local(self):
        from mcpbundles.config import CLISettings
        local = CLISettings.resolve(url_override="http://localhost:8000")
        assert local.is_local is True
        remote = CLISettings.resolve(url_override="https://api.mcpbundles.com")
        assert remote.is_local is False

    def test_require_auth_without_token(self, monkeypatch):
        from mcpbundles.config import CLISettings, AuthError
        monkeypatch.delenv("MCPBUNDLES_API_KEY", raising=False)
        with patch("mcpbundles.config.load_token", return_value=None):
            settings = CLISettings.resolve()
            with pytest.raises(AuthError, match="Not authenticated"):
                settings.require_auth()

    def test_require_auth_with_api_key_env(self, monkeypatch):
        from mcpbundles.config import CLISettings
        monkeypatch.setenv("MCPBUNDLES_API_KEY", "mb_env_key")
        settings = CLISettings.resolve()
        assert settings.require_auth() == "mb_env_key"

    def test_require_auth_with_stored_token(self, monkeypatch):
        from mcpbundles.config import CLISettings
        monkeypatch.delenv("MCPBUNDLES_API_KEY", raising=False)
        with patch("mcpbundles.config.load_token", return_value={"access_token": "stored_token"}):
            settings = CLISettings.resolve()
            assert settings.require_auth() == "stored_token"

    def test_env_key_takes_precedence_over_stored(self, monkeypatch):
        from mcpbundles.config import CLISettings
        monkeypatch.setenv("MCPBUNDLES_API_KEY", "mb_env_key")
        with patch("mcpbundles.config.load_token", return_value={"access_token": "stored_token"}):
            settings = CLISettings.resolve()
            assert settings.require_auth() == "mb_env_key"
