import pytest
from click.testing import CliRunner
from unittest.mock import patch, MagicMock
from cheshire_cat_cli.cli import cli, start, stop, update


@pytest.fixture
def runner():
    """Fixture to create a Click CLI test runner."""
    return CliRunner()


@pytest.fixture
def mock_home_dir(tmp_path):
    """Fixture to create a temporary home directory."""
    # Create compose.yml file in the temp directory
    compose_file = tmp_path / "compose.yml"
    compose_file.write_text("services:\n  test: {}")
    return tmp_path


class TestCLIGroup:
    """Tests for the main CLI group."""

    def test_cli_group_exists(self, runner):
        """Test that the main CLI group is accessible."""
        result = runner.invoke(cli, ["--help"])
        assert result.exit_code == 0
        assert "Chetshire-Cat AI cli manager utility." in result.output

    def test_cli_shows_all_commands(self, runner):
        """Test that all commands are listed in help."""
        result = runner.invoke(cli, ["--help"])
        assert result.exit_code == 0
        assert "start" in result.output
        assert "stop" in result.output
        assert "update" in result.output


class TestEnsureAppDirectory:
    """Tests for the _ensure_app_directory function."""

    @patch("cheshire_cat_cli.cli.files")
    @patch("cheshire_cat_cli.cli.click.get_app_dir")
    def test_ensure_app_directory_creates_directory(
        self, mock_get_app_dir, mock_files, tmp_path
    ):
        """Test that _ensure_app_directory creates the app directory."""
        from cheshire_cat_cli.cli import _ensure_app_directory

        app_dir = tmp_path / "ccat"
        mock_get_app_dir.return_value = str(app_dir)

        # Mock the compose.yml file
        mock_compose_file = MagicMock()
        mock_compose_file.open.return_value.__enter__.return_value.read.return_value = (
            b"test content"
        )
        mock_files.return_value.__truediv__.return_value = mock_compose_file

        result = _ensure_app_directory()

        assert result == app_dir
        assert app_dir.exists()
        mock_get_app_dir.assert_called_once_with("ccat", force_posix=True)

    @patch("cheshire_cat_cli.cli.files")
    @patch("cheshire_cat_cli.cli.click.get_app_dir")
    def test_ensure_app_directory_copies_compose_yml(
        self, mock_get_app_dir, mock_files, tmp_path
    ):
        """Test that _ensure_app_directory copies compose.yml if it doesn't exist."""
        from cheshire_cat_cli.cli import _ensure_app_directory

        app_dir = tmp_path / "ccat"
        app_dir.mkdir(parents=True, exist_ok=True)
        mock_get_app_dir.return_value = str(app_dir)

        # Mock the compose.yml file
        mock_compose_file = MagicMock()
        mock_compose_file.open.return_value.__enter__.return_value.read.return_value = (
            b"test compose content"
        )
        mock_files.return_value.__truediv__.return_value = mock_compose_file

        result = _ensure_app_directory()

        compose_target = app_dir / "compose.yml"
        assert compose_target.exists()
        assert compose_target.read_bytes() == b"test compose content"

    @patch("cheshire_cat_cli.cli.files")
    @patch("cheshire_cat_cli.cli.click.get_app_dir")
    def test_ensure_app_directory_skips_existing_compose_yml(
        self, mock_get_app_dir, mock_files, tmp_path
    ):
        """Test that _ensure_app_directory doesn't overwrite existing compose.yml."""
        from cheshire_cat_cli.cli import _ensure_app_directory

        app_dir = tmp_path / "ccat"
        app_dir.mkdir(parents=True, exist_ok=True)
        compose_target = app_dir / "compose.yml"
        compose_target.write_text("existing content")

        mock_get_app_dir.return_value = str(app_dir)

        result = _ensure_app_directory()

        # Should not have called files() since compose.yml already exists
        mock_files.assert_not_called()
        assert compose_target.read_text() == "existing content"


class TestStartCommand:
    """Tests for the start command."""

    def test_start_command_help(self, runner):
        """Test that the start command help is accessible."""
        result = runner.invoke(start, ["--help"])
        assert result.exit_code == 0
        assert "Start the Cheshire-Cat AI server." in result.output
        assert "--detach" in result.output or "-d" in result.output

    @patch("cheshire_cat_cli.cli.docker.check_docker_availability")
    @patch("cheshire_cat_cli.cli.os.execvp")
    @patch("cheshire_cat_cli.cli.os.chdir")
    @patch("cheshire_cat_cli.cli._ensure_app_directory")
    def test_start_command_without_detach(
        self,
        mock_ensure_app_dir,
        mock_chdir,
        mock_execvp,
        mock_docker_check,
        runner,
        mock_home_dir,
    ):
        """Test start command without detach flag."""
        mock_ensure_app_dir.return_value = mock_home_dir
        mock_docker_check.return_value = (True, "")

        result = runner.invoke(start)

        mock_docker_check.assert_called_once()
        mock_ensure_app_dir.assert_called_once()
        mock_chdir.assert_called_once_with(mock_home_dir)
        mock_execvp.assert_called_once_with("docker", ["docker", "compose", "up"])

    @patch("cheshire_cat_cli.cli.docker.check_docker_availability")
    @patch("cheshire_cat_cli.cli.os.execvp")
    @patch("cheshire_cat_cli.cli.os.chdir")
    @patch("cheshire_cat_cli.cli._ensure_app_directory")
    def test_start_command_with_detach(
        self,
        mock_ensure_app_dir,
        mock_chdir,
        mock_execvp,
        mock_docker_check,
        runner,
        mock_home_dir,
    ):
        """Test start command with detach flag."""
        mock_ensure_app_dir.return_value = mock_home_dir
        mock_docker_check.return_value = (True, "")

        result = runner.invoke(start, ["--detach"])

        mock_docker_check.assert_called_once()
        mock_ensure_app_dir.assert_called_once()
        mock_chdir.assert_called_once_with(mock_home_dir)
        mock_execvp.assert_called_once_with("docker", ["docker", "compose", "up", "-d"])

    @patch("cheshire_cat_cli.cli.docker.check_docker_availability")
    @patch("cheshire_cat_cli.cli.os.execvp")
    @patch("cheshire_cat_cli.cli.os.chdir")
    @patch("cheshire_cat_cli.cli._ensure_app_directory")
    def test_start_command_with_detach_short_flag(
        self,
        mock_ensure_app_dir,
        mock_chdir,
        mock_execvp,
        mock_docker_check,
        runner,
        mock_home_dir,
    ):
        """Test start command with detach short flag (-d)."""
        mock_ensure_app_dir.return_value = mock_home_dir
        mock_docker_check.return_value = (True, "")

        result = runner.invoke(start, ["-d"])

        mock_docker_check.assert_called_once()
        mock_ensure_app_dir.assert_called_once()
        mock_chdir.assert_called_once_with(mock_home_dir)
        mock_execvp.assert_called_once_with("docker", ["docker", "compose", "up", "-d"])

    @patch("cheshire_cat_cli.cli.docker.check_docker_availability")
    def test_start_command_docker_not_available(self, mock_docker_check, runner):
        """Test start command when Docker is not available."""
        mock_docker_check.return_value = (False, "Docker is not installed")

        result = runner.invoke(start)

        assert result.exit_code == 1
        assert "Error: Docker is not installed" in result.output
        mock_docker_check.assert_called_once()


class TestStopCommand:
    """Tests for the stop command."""

    def test_stop_command_help(self, runner):
        """Test that the stop command help is accessible."""
        result = runner.invoke(stop, ["--help"])
        assert result.exit_code == 0
        assert "Stop the Cheshire-Cat AI server." in result.output

    @patch("cheshire_cat_cli.cli.docker.check_docker_availability")
    @patch("cheshire_cat_cli.cli.os.execvp")
    @patch("cheshire_cat_cli.cli.os.chdir")
    @patch("cheshire_cat_cli.cli._ensure_app_directory")
    def test_stop_command(
        self,
        mock_ensure_app_dir,
        mock_chdir,
        mock_execvp,
        mock_docker_check,
        runner,
        mock_home_dir,
    ):
        """Test stop command executes correctly."""
        mock_ensure_app_dir.return_value = mock_home_dir
        mock_docker_check.return_value = (True, "")

        result = runner.invoke(stop)

        mock_docker_check.assert_called_once()
        mock_ensure_app_dir.assert_called_once()
        mock_chdir.assert_called_once_with(mock_home_dir)

    @patch("cheshire_cat_cli.cli.docker.check_docker_availability")
    def test_stop_command_docker_not_available(self, mock_docker_check, runner):
        """Test stop command when Docker is not available."""
        mock_docker_check.return_value = (False, "Docker daemon is not running")

        result = runner.invoke(stop)

        assert result.exit_code == 1
        assert "Error: Docker daemon is not running" in result.output
        mock_docker_check.assert_called_once()


class TestUpdateCommand:
    """Tests for the update command."""

    def test_update_command_help(self, runner):
        """Test that the update command help is accessible."""
        result = runner.invoke(update, ["--help"])
        assert result.exit_code == 0
        assert "Update the Cheshire-Cat AI server." in result.output

    @patch("cheshire_cat_cli.cli.docker.check_docker_availability")
    @patch("cheshire_cat_cli.cli.os.execvp")
    @patch("cheshire_cat_cli.cli.os.chdir")
    @patch("cheshire_cat_cli.cli._ensure_app_directory")
    def test_update_command(
        self,
        mock_ensure_app_dir,
        mock_chdir,
        mock_execvp,
        mock_docker_check,
        runner,
        mock_home_dir,
    ):
        """Test update command executes correctly."""
        mock_ensure_app_dir.return_value = mock_home_dir
        mock_docker_check.return_value = (True, "")

        result = runner.invoke(update)

        mock_docker_check.assert_called_once()
        mock_ensure_app_dir.assert_called_once()
        mock_chdir.assert_called_once_with(mock_home_dir)
        mock_execvp.assert_called_once_with(
            "docker", ["docker", "pull", "ghcr.io/cheshire-cat-ai/core"]
        )

    @patch("cheshire_cat_cli.cli.docker.check_docker_availability")
    def test_update_command_docker_not_available(self, mock_docker_check, runner):
        """Test update command when Docker is not available."""
        mock_docker_check.return_value = (False, "Docker is not installed")

        result = runner.invoke(update)

        assert result.exit_code == 1
        assert "Error: Docker is not installed" in result.output
        mock_docker_check.assert_called_once()


class TestIntegration:
    """Integration tests for the CLI."""

    def test_cli_invokes_start_command(self, runner):
        """Test that start command can be invoked through main CLI."""
        result = runner.invoke(cli, ["start", "--help"])
        assert result.exit_code == 0
        assert "Start the Cheshire-Cat AI server." in result.output

    def test_cli_invokes_stop_command(self, runner):
        """Test that stop command can be invoked through main CLI."""
        result = runner.invoke(cli, ["stop", "--help"])
        assert result.exit_code == 0
        assert "Stop the Cheshire-Cat AI server." in result.output

    def test_cli_invokes_update_command(self, runner):
        """Test that update command can be invoked through main CLI."""
        result = runner.invoke(cli, ["update", "--help"])
        assert result.exit_code == 0
        assert "Update the Cheshire-Cat AI server." in result.output

    def test_cli_handles_invalid_command(self, runner):
        """Test that CLI handles invalid commands gracefully."""
        result = runner.invoke(cli, ["invalid-command"])
        assert result.exit_code != 0
        assert "Error" in result.output or "No such command" in result.output
