"""Tests for exokit CLI commands.

Uses ``typer.testing.CliRunner`` to invoke CLI commands in-process.
All external dependencies (subprocess calls, file I/O from APX/skills)
are mocked to keep tests fast and isolated.
"""

from __future__ import annotations

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

from typer.testing import CliRunner

from exokit.cli import cli_app as app
from exokit.core.models import PrereqResult, QuestionnaireAnswers

runner = CliRunner()


# ---------------------------------------------------------------------------
# Help text
# ---------------------------------------------------------------------------


class TestHelpOutput:
    """Verify exokit --help shows all commands."""

    def test_help_shows_all_commands(self) -> None:
        """--help should list init, validate, and update-skills."""
        result = runner.invoke(app, ["--help"])
        assert result.exit_code == 0
        assert "init" in result.output
        assert "validate" in result.output
        assert "update-skills" in result.output

    def test_help_shows_app_name(self) -> None:
        """--help should show the app name and description."""
        result = runner.invoke(app, ["--help"])
        assert "Databricks Demo Template Generator" in result.output

    def test_init_help(self) -> None:
        """init --help should show usage information."""
        result = runner.invoke(app, ["init", "--help"])
        assert result.exit_code == 0
        assert "Scaffold a new demo project" in result.output

    def test_validate_help(self) -> None:
        """validate --help should show usage information."""
        result = runner.invoke(app, ["validate", "--help"])
        assert result.exit_code == 0
        assert "Check prerequisites" in result.output

    def test_update_skills_help(self) -> None:
        """update-skills --help should show usage information."""
        result = runner.invoke(app, ["update-skills", "--help"])
        assert result.exit_code == 0
        assert "Refresh bundled skills" in result.output


# ---------------------------------------------------------------------------
# validate command
# ---------------------------------------------------------------------------


class TestValidateCommand:
    """Tests for the ``exokit validate`` command."""

    @patch("exokit.cli.check_all")
    def test_validate_runs_prereq_checks(self, mock_check_all: MagicMock) -> None:
        """validate should call check_all and display results."""
        mock_check_all.return_value = [
            PrereqResult(name="databricks-cli", passed=True, message="Found", version="0.18.0"),
            PrereqResult(name="uv", passed=True, message="Found", version="0.4.0"),
            PrereqResult(name="node", passed=True, message="Found", version="20.0.0"),
            PrereqResult(name="apx", passed=False, message="Not found"),
            PrereqResult(name="databricks-auth-DEFAULT", passed=True, message="Configured"),
        ]

        result = runner.invoke(app, ["validate"])

        assert result.exit_code == 0
        mock_check_all.assert_called_once()
        assert "databricks-cli" in result.output
        assert "PASS" in result.output
        assert "FAIL" in result.output

    @patch("exokit.cli.check_all")
    def test_validate_all_pass(self, mock_check_all: MagicMock) -> None:
        """When all checks pass, show success message."""
        mock_check_all.return_value = [
            PrereqResult(name="databricks-cli", passed=True, message="Found"),
            PrereqResult(name="uv", passed=True, message="Found"),
        ]

        result = runner.invoke(app, ["validate"])

        assert result.exit_code == 0
        assert "checks passed" in result.output

    @patch("exokit.cli.check_all")
    def test_validate_with_profile(self, mock_check_all: MagicMock) -> None:
        """validate --profile should pass the profile to check_all."""
        mock_check_all.return_value = [
            PrereqResult(name="test", passed=True, message="OK"),
        ]

        result = runner.invoke(app, ["validate", "--profile", "STAGING"])

        assert result.exit_code == 0
        mock_check_all.assert_called_once_with(databricks_profile="STAGING")

    @patch("exokit.cli.check_all")
    def test_validate_default_profile(self, mock_check_all: MagicMock) -> None:
        """validate without --profile should use DEFAULT."""
        mock_check_all.return_value = [
            PrereqResult(name="test", passed=True, message="OK"),
        ]

        runner.invoke(app, ["validate"])
        mock_check_all.assert_called_once_with(databricks_profile="DEFAULT")


# ---------------------------------------------------------------------------
# update-skills command
# ---------------------------------------------------------------------------


class TestUpdateSkillsCommand:
    """Tests for the ``exokit update-skills`` command."""

    @patch("exokit.cli.core_update_skills")
    def test_update_skills_calls_core(self, mock_update: MagicMock, tmp_path: Path) -> None:
        """update-skills should call core.skills.update_skills."""
        mock_update.return_value = ["databricks-jobs", "demo-governance"]

        result = runner.invoke(app, ["update-skills", str(tmp_path)])

        assert result.exit_code == 0
        mock_update.assert_called_once_with(tmp_path, skills_source=None)
        assert "Updated 2 skills" in result.output

    @patch("exokit.cli.core_update_skills")
    def test_update_skills_with_source(self, mock_update: MagicMock, tmp_path: Path) -> None:
        """update-skills --source should pass source to core function."""
        source = tmp_path / "skills"
        source.mkdir()
        mock_update.return_value = []

        result = runner.invoke(app, ["update-skills", str(tmp_path), "--source", str(source)])

        assert result.exit_code == 0
        mock_update.assert_called_once_with(tmp_path, skills_source=source)

    @patch("exokit.cli.core_update_skills")
    def test_update_skills_no_updates(self, mock_update: MagicMock, tmp_path: Path) -> None:
        """When no skills are updated, show warning message."""
        mock_update.return_value = []

        result = runner.invoke(app, ["update-skills", str(tmp_path)])

        assert result.exit_code == 0
        assert "No skills updated" in result.output


# ---------------------------------------------------------------------------
# init command — with mocked wizard and scaffold
# ---------------------------------------------------------------------------


class TestInitCommand:
    """Tests for the ``exokit init`` command.

    The TUI wizard and scaffold pipeline are mocked since they require
    a terminal and filesystem operations respectively.
    """

    @patch("exokit.cli.questionary")
    @patch("exokit.cli._run_scaffold")
    @patch("exokit.cli._run_wizard")
    @patch("exokit.cli.check_all")
    def test_init_calls_wizard_and_scaffold(
        self,
        mock_check_all: MagicMock,
        mock_wizard: MagicMock,
        mock_scaffold: MagicMock,
        mock_questionary: MagicMock,
        tmp_path: Path,
    ) -> None:
        """init should call prereqs, wizard, and scaffold."""
        mock_check_all.return_value = [
            PrereqResult(name="uv", passed=True, message="Found"),
        ]
        answers = QuestionnaireAnswers(
            company="TestCo",
            industry="fsi",
            demo_description="Test demo description",
            catalog="testco_fsi",
            warehouse_id="wh123",
            workspace_host="https://test.databricks.net",
            notification_email="test@test.com",
        )
        mock_wizard.return_value = answers
        mock_questionary.confirm.return_value.ask.return_value = True

        output_dir = tmp_path / "output"
        result = runner.invoke(app, ["init", str(output_dir)])

        assert result.exit_code == 0
        mock_check_all.assert_called_once()
        mock_wizard.assert_called_once()
        mock_scaffold.assert_called_once_with(answers, output_dir)

    @patch("exokit.cli._run_scaffold")
    @patch("exokit.cli._run_wizard")
    @patch("exokit.cli.check_all")
    def test_init_cancelled_by_user(
        self,
        mock_check_all: MagicMock,
        mock_wizard: MagicMock,
        mock_scaffold: MagicMock,
        tmp_path: Path,
    ) -> None:
        """init should exit gracefully when wizard returns None (cancelled)."""
        mock_check_all.return_value = []
        mock_wizard.return_value = None  # User pressed Escape

        output_dir = tmp_path / "output"
        result = runner.invoke(app, ["init", str(output_dir)])

        assert result.exit_code == 0
        assert "Cancelled" in result.output
        mock_scaffold.assert_not_called()

    def test_init_rejects_generator_repo(self, tmp_path: Path) -> None:
        """init should refuse to scaffold into the exokit repo itself."""
        # Create the marker that identifies the generator repo
        (tmp_path / "src" / "exokit").mkdir(parents=True)

        result = runner.invoke(app, ["init", str(tmp_path)])

        assert result.exit_code == 1
        assert "exokit generator repo" in result.output

    @patch("exokit.cli.questionary")
    @patch("exokit.cli._run_scaffold")
    @patch("exokit.cli._run_wizard")
    @patch("exokit.cli.check_all")
    def test_init_warns_on_failed_prereqs(
        self,
        mock_check_all: MagicMock,
        mock_wizard: MagicMock,
        mock_scaffold: MagicMock,
        mock_questionary: MagicMock,
        tmp_path: Path,
    ) -> None:
        """init should warn about failed prereqs but still continue."""
        mock_check_all.return_value = [
            PrereqResult(name="apx", passed=False, message="Not found"),
        ]
        mock_wizard.return_value = QuestionnaireAnswers(
            company="TestCo",
            industry="fsi",
            demo_description="Test demo description",
            catalog="testco_fsi",
            warehouse_id="wh123",
            workspace_host="https://test.databricks.net",
            notification_email="test@test.com",
        )
        mock_questionary.confirm.return_value.ask.return_value = True

        output_dir = tmp_path / "output"
        result = runner.invoke(app, ["init", str(output_dir)])

        assert result.exit_code == 0
        assert "not met" in result.output
        mock_wizard.assert_called_once()
