"""Test new CLI features: exit codes, config reset, install-completion,
serve helpers, shell session, --stream, --quiet."""

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):
            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):
            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/")
        assert t.name == "hub"
        assert t.url == "https://api.mcpbundles.com"
        assert t.endpoint == "/hub/"

    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("auth", 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("auth", 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("auth", ShellCompleter())
        asyncio.run(s.disconnect())

    def test_connect_cleans_up_on_failure(self):
        from mcpbundles.commands.shell_cmd import _ShellSession, ShellCompleter
        s = _ShellSession("fake_auth", ShellCompleter())
        with pytest.raises(Exception):
            asyncio.run(s.connect("https://api.mcpbundles.com", "/hub/", "test"))
        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"
