"""Tests for shell integration module."""

from __future__ import annotations

import os
from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest
from typer.testing import CliRunner

from gaol.shell import (
    CompletionGenerator,
    ContainerContext,
    PromptIntegration,
    Shell,
    get_supported_shells,
)


class TestShellEnum:
    """Tests for Shell enum."""

    def test_shell_values(self) -> None:
        """Test Shell enum values."""
        assert Shell.BASH.value == "bash"
        assert Shell.ZSH.value == "zsh"
        assert Shell.FISH.value == "fish"

    def test_shell_from_string(self) -> None:
        """Test creating Shell from string."""
        assert Shell("bash") == Shell.BASH
        assert Shell("zsh") == Shell.ZSH
        assert Shell("fish") == Shell.FISH

    def test_shell_invalid_string(self) -> None:
        """Test invalid shell string raises ValueError."""
        with pytest.raises(ValueError):
            Shell("invalid")


class TestGetSupportedShells:
    """Tests for get_supported_shells function."""

    def test_returns_list(self) -> None:
        """Test returns list of shell names."""
        shells = get_supported_shells()
        assert isinstance(shells, list)
        assert len(shells) == 3

    def test_contains_all_shells(self) -> None:
        """Test contains all supported shells."""
        shells = get_supported_shells()
        assert "bash" in shells
        assert "zsh" in shells
        assert "fish" in shells


class TestCompletionGenerator:
    """Tests for CompletionGenerator class."""

    def test_generate_bash(self) -> None:
        """Test generating bash completion script."""
        gen = CompletionGenerator()
        script = gen.generate(Shell.BASH)
        assert "# Bash completion for gaol" in script
        assert "_gaol_completion" in script
        assert "complete -o nosort" in script

    def test_generate_bash_from_string(self) -> None:
        """Test generating bash completion from string."""
        gen = CompletionGenerator()
        script = gen.generate("bash")
        assert "# Bash completion for gaol" in script

    def test_generate_zsh(self) -> None:
        """Test generating zsh completion script."""
        gen = CompletionGenerator()
        script = gen.generate(Shell.ZSH)
        assert "#compdef gaol" in script
        assert "_gaol_completion" in script
        assert "compdef" in script

    def test_generate_zsh_from_string(self) -> None:
        """Test generating zsh completion from string."""
        gen = CompletionGenerator()
        script = gen.generate("zsh")
        assert "#compdef gaol" in script

    def test_generate_fish(self) -> None:
        """Test generating fish completion script."""
        gen = CompletionGenerator()
        script = gen.generate(Shell.FISH)
        assert "# Fish completion for gaol" in script
        assert "function _gaol_completion" in script
        assert "complete -c gaol" in script

    def test_generate_fish_from_string(self) -> None:
        """Test generating fish completion from string."""
        gen = CompletionGenerator()
        script = gen.generate("fish")
        assert "# Fish completion for gaol" in script

    def test_generate_invalid_shell(self) -> None:
        """Test generating with invalid shell raises error."""
        gen = CompletionGenerator()
        with pytest.raises(ValueError, match="is not a valid Shell"):
            gen.generate("invalid")

    def test_get_install_path_bash(self) -> None:
        """Test getting bash completion install path."""
        gen = CompletionGenerator()
        path = gen.get_install_path(Shell.BASH)
        assert path.name == "gaol"
        assert "bash-completion" in str(path)
        assert "completions" in str(path)

    def test_get_install_path_zsh(self) -> None:
        """Test getting zsh completion install path."""
        gen = CompletionGenerator()
        path = gen.get_install_path(Shell.ZSH)
        assert path.name == "_gaol"
        assert ".zsh" in str(path)
        assert "completions" in str(path)

    def test_get_install_path_fish(self) -> None:
        """Test getting fish completion install path."""
        gen = CompletionGenerator()
        path = gen.get_install_path(Shell.FISH)
        assert path.name == "gaol.fish"
        assert "fish" in str(path)
        assert "completions" in str(path)

    def test_get_install_path_from_string(self) -> None:
        """Test getting install path from string."""
        gen = CompletionGenerator()
        path = gen.get_install_path("bash")
        assert path.name == "gaol"

    def test_get_install_path_respects_xdg_data_home(self) -> None:
        """Test bash install path respects XDG_DATA_HOME."""
        gen = CompletionGenerator()
        with patch.dict(os.environ, {"XDG_DATA_HOME": "/custom/data"}):
            path = gen.get_install_path(Shell.BASH)
            assert str(path).startswith("/custom/data")

    def test_get_install_path_respects_xdg_config_home(self) -> None:
        """Test fish install path respects XDG_CONFIG_HOME."""
        gen = CompletionGenerator()
        with patch.dict(os.environ, {"XDG_CONFIG_HOME": "/custom/config"}):
            path = gen.get_install_path(Shell.FISH)
            assert str(path).startswith("/custom/config")

    def test_install_creates_file(self, tmp_path: Path) -> None:
        """Test install creates completion file."""
        gen = CompletionGenerator()

        # Patch get_install_path to return tmp_path
        install_path = tmp_path / "completions" / "gaol"
        with patch.object(gen, "get_install_path", return_value=install_path):
            result = gen.install(Shell.BASH)

            assert result == install_path
            assert install_path.exists()
            content = install_path.read_text()
            assert "# Bash completion for gaol" in content


class TestPromptIntegration:
    """Tests for PromptIntegration class."""

    def test_generate_bash(self) -> None:
        """Test generating bash prompt integration."""
        prompt = PromptIntegration()
        script = prompt.generate(Shell.BASH)
        assert "# Gaol shell integration for Bash" in script
        assert "gaol_prompt()" in script
        assert "muse()" in script
        assert "munuse()" in script
        assert "msh()" in script
        assert "mstatus()" in script
        assert "mexec()" in script
        assert "GAOL_CONTAINER" in script

    def test_generate_bash_from_string(self) -> None:
        """Test generating bash prompt from string."""
        prompt = PromptIntegration()
        script = prompt.generate("bash")
        assert "# Gaol shell integration for Bash" in script

    def test_generate_zsh(self) -> None:
        """Test generating zsh prompt integration."""
        prompt = PromptIntegration()
        script = prompt.generate(Shell.ZSH)
        assert "# Gaol shell integration for Zsh" in script
        assert "gaol_prompt()" in script
        assert "muse()" in script
        assert "munuse()" in script
        assert "[[ -n" in script  # Zsh syntax

    def test_generate_zsh_from_string(self) -> None:
        """Test generating zsh prompt from string."""
        prompt = PromptIntegration()
        script = prompt.generate("zsh")
        assert "# Gaol shell integration for Zsh" in script

    def test_generate_fish(self) -> None:
        """Test generating fish prompt integration."""
        prompt = PromptIntegration()
        script = prompt.generate(Shell.FISH)
        assert "# Gaol shell integration for Fish" in script
        assert "function gaol_prompt" in script
        assert "function muse" in script
        assert "function munuse" in script
        assert "function msh" in script
        assert "set -gx GAOL_CONTAINER" in script

    def test_generate_fish_from_string(self) -> None:
        """Test generating fish prompt from string."""
        prompt = PromptIntegration()
        script = prompt.generate("fish")
        assert "# Gaol shell integration for Fish" in script

    def test_generate_invalid_shell(self) -> None:
        """Test generating with invalid shell raises error."""
        prompt = PromptIntegration()
        with pytest.raises(ValueError, match="is not a valid Shell"):
            prompt.generate("invalid")

    def test_bash_contains_mstart_mstop(self) -> None:
        """Test bash script contains mstart and mstop."""
        prompt = PromptIntegration()
        script = prompt.generate(Shell.BASH)
        assert "mstart()" in script
        assert "mstop()" in script
        assert "mlogs()" in script

    def test_fish_contains_all_functions(self) -> None:
        """Test fish script contains all helper functions."""
        prompt = PromptIntegration()
        script = prompt.generate(Shell.FISH)
        assert "function mstart" in script
        assert "function mstop" in script
        assert "function mlogs" in script


class TestContainerContext:
    """Tests for ContainerContext class."""

    def test_env_var_name(self) -> None:
        """Test environment variable name."""
        assert ContainerContext.ENV_VAR == "GAOL_CONTAINER"

    def test_get_current_when_not_set(self) -> None:
        """Test get_current returns None when not set."""
        with patch.dict(os.environ, {}, clear=True):
            os.environ.pop(ContainerContext.ENV_VAR, None)
            result = ContainerContext.get_current()
            assert result is None

    def test_get_current_when_set(self) -> None:
        """Test get_current returns value when set."""
        with patch.dict(os.environ, {ContainerContext.ENV_VAR: "mycontainer"}):
            result = ContainerContext.get_current()
            assert result == "mycontainer"

    def test_get_status_short_unknown_container(self) -> None:
        """Test get_status_short returns ? for unknown container."""
        with patch("gaol.shell.context.get_container_store") as mock_store:
            mock_store.return_value.get.return_value = None
            result = ContainerContext.get_status_short("unknown")
            assert result == "?"

    def test_get_status_short_with_runtime_error(self) -> None:
        """Test get_status_short handles runtime errors."""
        with patch("gaol.shell.context.get_container_store") as mock_store:
            mock_stored = MagicMock()
            mock_stored.runtime = "docker"
            mock_store.return_value.get.return_value = mock_stored

            with patch("gaol.shell.context.get_runtime") as mock_rt:
                mock_rt.side_effect = Exception("Runtime error")
                result = ContainerContext.get_status_short("mycontainer")
                assert result == "?"

    def test_get_status_short_returns_status(self) -> None:
        """Test get_status_short returns actual status."""
        with patch("gaol.shell.context.get_container_store") as mock_store:
            mock_stored = MagicMock()
            mock_stored.runtime = "docker"
            mock_store.return_value.get.return_value = mock_stored

            with patch("gaol.shell.context.get_runtime") as mock_rt:
                mock_state = MagicMock()
                mock_state.status.value = "running"
                mock_rt.return_value.status.return_value = mock_state
                result = ContainerContext.get_status_short("mycontainer")
                assert result == "running"

    def test_get_context_info_unknown_container(self) -> None:
        """Test get_context_info for unknown container."""
        with patch("gaol.shell.context.get_container_store") as mock_store:
            mock_store.return_value.get.return_value = None
            info = ContainerContext.get_context_info("unknown")
            assert info["name"] == "unknown"
            assert info["runtime"] is None
            assert info["status"] is None
            assert info["image"] is None
            assert info["ip_address"] is None

    def test_get_context_info_with_container(self) -> None:
        """Test get_context_info with known container."""
        with patch("gaol.shell.context.get_container_store") as mock_store:
            mock_config = MagicMock()
            mock_config.image = "debian:bookworm"
            mock_stored = MagicMock()
            mock_stored.runtime = "docker"
            mock_stored.config = mock_config
            mock_store.return_value.get.return_value = mock_stored

            with patch("gaol.shell.context.get_runtime") as mock_rt:
                mock_state = MagicMock()
                mock_state.status.value = "running"
                mock_state.ip_address = "172.17.0.2"
                mock_rt.return_value.status.return_value = mock_state

                info = ContainerContext.get_context_info("mycontainer")
                assert info["name"] == "mycontainer"
                assert info["runtime"] == "docker"
                assert info["status"] == "running"
                assert info["image"] == "debian:bookworm"
                assert info["ip_address"] == "172.17.0.2"

    def test_list_containers(self) -> None:
        """Test list_containers returns container names."""
        with patch("gaol.shell.context.get_container_store") as mock_store:
            mock_store.return_value.list.return_value = ["container1", "container2"]
            result = ContainerContext.list_containers()
            assert result == ["container1", "container2"]

    def test_container_exists_true(self) -> None:
        """Test container_exists returns True for existing container."""
        with patch("gaol.shell.context.get_container_store") as mock_store:
            mock_store.return_value.get.return_value = MagicMock()
            result = ContainerContext.container_exists("mycontainer")
            assert result is True

    def test_container_exists_false(self) -> None:
        """Test container_exists returns False for unknown container."""
        with patch("gaol.shell.context.get_container_store") as mock_store:
            mock_store.return_value.get.return_value = None
            result = ContainerContext.container_exists("unknown")
            assert result is False


class TestShellIntegrationCLI:
    """Tests for shell-integration CLI commands."""

    @pytest.fixture
    def runner(self) -> CliRunner:
        """Create CLI test runner."""
        return CliRunner()

    def test_completions_bash(self, runner: CliRunner) -> None:
        """Test completions command for bash."""
        from gaol.cli import app

        result = runner.invoke(app, ["shell-integration", "completions", "bash"])
        assert result.exit_code == 0
        assert "# Bash completion for gaol" in result.output

    def test_completions_zsh(self, runner: CliRunner) -> None:
        """Test completions command for zsh."""
        from gaol.cli import app

        result = runner.invoke(app, ["shell-integration", "completions", "zsh"])
        assert result.exit_code == 0
        assert "#compdef gaol" in result.output

    def test_completions_fish(self, runner: CliRunner) -> None:
        """Test completions command for fish."""
        from gaol.cli import app

        result = runner.invoke(app, ["shell-integration", "completions", "fish"])
        assert result.exit_code == 0
        assert "# Fish completion for gaol" in result.output

    def test_completions_invalid_shell(self, runner: CliRunner) -> None:
        """Test completions command with invalid shell."""
        from gaol.cli import app

        result = runner.invoke(app, ["shell-integration", "completions", "invalid"])
        assert result.exit_code == 1
        assert "Unsupported shell" in result.output

    def test_completions_install(self, runner: CliRunner, tmp_path: Path) -> None:
        """Test completions install option."""
        from gaol.cli import app

        install_path = tmp_path / "completions" / "gaol"
        with patch(
            "gaol.shell.CompletionGenerator.get_install_path",
            return_value=install_path,
        ):
            result = runner.invoke(
                app, ["shell-integration", "completions", "bash", "--install"]
            )
            assert result.exit_code == 0
            assert "Completions installed to" in result.output
            assert install_path.exists()

    def test_prompt_bash(self, runner: CliRunner) -> None:
        """Test prompt command for bash."""
        from gaol.cli import app

        result = runner.invoke(app, ["shell-integration", "prompt", "bash"])
        assert result.exit_code == 0
        assert "# Gaol shell integration for Bash" in result.output
        assert "gaol_prompt()" in result.output

    def test_prompt_zsh(self, runner: CliRunner) -> None:
        """Test prompt command for zsh."""
        from gaol.cli import app

        result = runner.invoke(app, ["shell-integration", "prompt", "zsh"])
        assert result.exit_code == 0
        assert "# Gaol shell integration for Zsh" in result.output

    def test_prompt_fish(self, runner: CliRunner) -> None:
        """Test prompt command for fish."""
        from gaol.cli import app

        result = runner.invoke(app, ["shell-integration", "prompt", "fish"])
        assert result.exit_code == 0
        assert "# Gaol shell integration for Fish" in result.output

    def test_prompt_invalid_shell(self, runner: CliRunner) -> None:
        """Test prompt command with invalid shell."""
        from gaol.cli import app

        result = runner.invoke(app, ["shell-integration", "prompt", "invalid"])
        assert result.exit_code == 1
        assert "Unsupported shell" in result.output

    def test_init_bash(self, runner: CliRunner) -> None:
        """Test init command for bash."""
        from gaol.cli import app

        result = runner.invoke(app, ["shell-integration", "init", "bash"])
        assert result.exit_code == 0
        # Should contain both completions and prompt
        assert "# Bash completion for gaol" in result.output
        assert "# Gaol shell integration for Bash" in result.output

    def test_init_zsh(self, runner: CliRunner) -> None:
        """Test init command for zsh."""
        from gaol.cli import app

        result = runner.invoke(app, ["shell-integration", "init", "zsh"])
        assert result.exit_code == 0
        assert "#compdef gaol" in result.output
        assert "# Gaol shell integration for Zsh" in result.output

    def test_init_fish(self, runner: CliRunner) -> None:
        """Test init command for fish."""
        from gaol.cli import app

        result = runner.invoke(app, ["shell-integration", "init", "fish"])
        assert result.exit_code == 0
        assert "# Fish completion for gaol" in result.output
        assert "# Gaol shell integration for Fish" in result.output

    def test_init_invalid_shell(self, runner: CliRunner) -> None:
        """Test init command with invalid shell."""
        from gaol.cli import app

        result = runner.invoke(app, ["shell-integration", "init", "invalid"])
        assert result.exit_code == 1
        assert "Unsupported shell" in result.output

    def test_context_no_args(self, runner: CliRunner) -> None:
        """Test context command with no arguments."""
        from gaol.cli import app

        with patch.dict(os.environ, {}, clear=True):
            os.environ.pop("GAOL_CONTAINER", None)
            result = runner.invoke(app, ["shell-integration", "context"])
            assert result.exit_code == 0
            assert "No container context set" in result.output

    def test_context_with_env_var(self, runner: CliRunner) -> None:
        """Test context command with env var set."""
        from gaol.cli import app

        with patch.dict(os.environ, {"GAOL_CONTAINER": "mycontainer"}):
            with patch("gaol.shell.ContainerContext.get_context_info") as mock_info:
                mock_info.return_value = {
                    "name": "mycontainer",
                    "runtime": "docker",
                    "status": "running",
                    "image": "debian:bookworm",
                    "ip_address": None,
                }
                result = runner.invoke(app, ["shell-integration", "context"])
                assert result.exit_code == 0
                assert "mycontainer" in result.output

    def test_context_with_container_arg(self, runner: CliRunner) -> None:
        """Test context command with container argument."""
        from gaol.cli import app

        with patch("gaol.shell.ContainerContext.get_context_info") as mock_info:
            mock_info.return_value = {
                "name": "testcontainer",
                "runtime": "docker",
                "status": "stopped",
                "image": None,
                "ip_address": None,
            }
            result = runner.invoke(app, ["shell-integration", "context", "testcontainer"])
            assert result.exit_code == 0
            assert "testcontainer" in result.output
            assert "docker" in result.output

    def test_context_short_format(self, runner: CliRunner) -> None:
        """Test context command with --short option."""
        from gaol.cli import app

        with patch.dict(os.environ, {"GAOL_CONTAINER": "mycontainer"}):
            with patch(
                "gaol.shell.ContainerContext.get_status_short"
            ) as mock_status:
                mock_status.return_value = "running"
                result = runner.invoke(
                    app, ["shell-integration", "context", "--short"]
                )
                assert result.exit_code == 0
                assert "running" in result.output

    def test_context_format_short(self, runner: CliRunner) -> None:
        """Test context command with --format short option."""
        from gaol.cli import app

        with patch.dict(os.environ, {"GAOL_CONTAINER": "mycontainer"}):
            with patch(
                "gaol.shell.ContainerContext.get_status_short"
            ) as mock_status:
                mock_status.return_value = "stopped"
                result = runner.invoke(
                    app, ["shell-integration", "context", "--format", "short"]
                )
                assert result.exit_code == 0
                assert "stopped" in result.output

    def test_context_clear_option(self, runner: CliRunner) -> None:
        """Test context command with --clear option."""
        from gaol.cli import app

        result = runner.invoke(app, ["shell-integration", "context", "--clear"])
        assert result.exit_code == 0
        assert "munuse" in result.output or "unset GAOL_CONTAINER" in result.output


class TestModuleExports:
    """Tests for shell module exports."""

    def test_imports(self) -> None:
        """Test all expected classes are importable."""
        from gaol.shell import (
            CompletionGenerator,
            ContainerContext,
            PromptIntegration,
            Shell,
            get_supported_shells,
        )

        assert CompletionGenerator is not None
        assert ContainerContext is not None
        assert PromptIntegration is not None
        assert Shell is not None
        assert get_supported_shells is not None
