"""Tests for the ``jarvis skill`` CLI commands."""

from __future__ import annotations

import textwrap
from pathlib import Path
from unittest.mock import patch

from click.testing import CliRunner

from openjarvis.cli import cli


class TestSkillCmd:
    def test_skill_list_help(self) -> None:
        result = CliRunner().invoke(cli, ["skill", "list", "--help"])
        assert result.exit_code == 0

    def test_skill_install_help(self) -> None:
        result = CliRunner().invoke(cli, ["skill", "install", "--help"])
        assert result.exit_code == 0

    def test_skill_search_help(self) -> None:
        result = CliRunner().invoke(cli, ["skill", "search", "--help"])
        assert result.exit_code == 0

    def test_skill_group_help(self) -> None:
        result = CliRunner().invoke(cli, ["skill", "--help"])
        assert result.exit_code == 0
        assert "list" in result.output
        assert "install" in result.output
        assert "remove" in result.output
        assert "search" in result.output

    def test_skill_run_help(self) -> None:
        result = CliRunner().invoke(cli, ["skill", "run", "--help"])
        assert result.exit_code == 0

    def test_skill_info_help(self) -> None:
        result = CliRunner().invoke(cli, ["skill", "info", "--help"])
        assert result.exit_code == 0

    def test_skill_update_help(self) -> None:
        result = CliRunner().invoke(cli, ["skill", "update", "--help"])
        assert result.exit_code == 0

    def test_skill_list_shows_discovered_skills(self, tmp_path: Path) -> None:
        skill_dir = tmp_path / "my_skill"
        skill_dir.mkdir()
        (skill_dir / "skill.toml").write_text(
            textwrap.dedent("""\
            [skill]
            name = "my_skill"
            description = "A test skill"

            [[skill.steps]]
            tool_name = "echo"
            output_key = "x"
        """)
        )
        with patch(
            "openjarvis.cli.skill_cmd._get_skill_paths",
            return_value=[tmp_path],
        ):
            result = CliRunner().invoke(cli, ["skill", "list"])
            assert result.exit_code == 0
            assert "my_skill" in result.output

    def test_skill_info_shows_details(self, tmp_path: Path) -> None:
        skill_dir = tmp_path / "info_skill"
        skill_dir.mkdir()
        (skill_dir / "skill.toml").write_text(
            textwrap.dedent("""\
            [skill]
            name = "info_skill"
            description = "Detailed skill"
            author = "test_author"
            tags = ["research", "test"]
            required_capabilities = ["network:fetch"]

            [[skill.steps]]
            tool_name = "echo"
            output_key = "x"
        """)
        )
        with patch(
            "openjarvis.cli.skill_cmd._get_skill_paths",
            return_value=[tmp_path],
        ):
            result = CliRunner().invoke(cli, ["skill", "info", "info_skill"])
            assert result.exit_code == 0
            assert "info_skill" in result.output
            assert "test_author" in result.output


class TestSkillRemoveCommand:
    def test_remove_help(self) -> None:
        result = CliRunner().invoke(cli, ["skill", "remove", "--help"])
        assert result.exit_code == 0

    def test_remove_missing_skill(self, tmp_path: Path) -> None:
        with patch(
            "openjarvis.cli.skill_cmd._get_skill_paths",
            return_value=[tmp_path],
        ):
            result = CliRunner().invoke(cli, ["skill", "remove", "ghost", "--yes"])
            assert result.exit_code != 0
            assert "no installed skill" in result.output.lower()

    def test_remove_deletes_directory(self, tmp_path: Path) -> None:
        skill_dir = tmp_path / "to_remove"
        skill_dir.mkdir()
        (skill_dir / "skill.toml").write_text(
            textwrap.dedent("""\
            [skill]
            name = "to_remove"
            description = "doomed"

            [[skill.steps]]
            tool_name = "echo"
            output_key = "x"
        """)
        )
        with patch(
            "openjarvis.cli.skill_cmd._get_skill_paths",
            return_value=[tmp_path],
        ):
            result = CliRunner().invoke(cli, ["skill", "remove", "to_remove", "--yes"])
            assert result.exit_code == 0, result.output
            assert "Removed" in result.output
            assert not skill_dir.exists()


class TestSkillSearchCommand:
    def test_search_help(self) -> None:
        result = CliRunner().invoke(cli, ["skill", "search", "--help"])
        assert result.exit_code == 0

    def test_search_no_sources_configured(self) -> None:
        from openjarvis.core.config import JarvisConfig, SkillsConfig

        cfg = JarvisConfig()
        cfg.skills = SkillsConfig(sources=[])
        with patch("openjarvis.cli.skill_cmd.load_config", return_value=cfg):
            result = CliRunner().invoke(cli, ["skill", "search", "anything"])
            assert result.exit_code != 0
            assert "no skill sources" in result.output.lower()

    def test_search_filters_results(self) -> None:
        from pathlib import Path as _P

        from openjarvis.core.config import (
            JarvisConfig,
            SkillsConfig,
            SkillSourceConfig,
        )
        from openjarvis.skills.sources.base import ResolvedSkill

        class _FakeResolver:
            def sync(self) -> None:
                return None

            def list_skills(self):
                return [
                    ResolvedSkill(
                        name="apple-notes",
                        source="hermes",
                        path=_P("/tmp"),
                        category="apple",
                        description="Take notes on macOS",
                        commit="abc",
                    ),
                    ResolvedSkill(
                        name="github-prs",
                        source="hermes",
                        path=_P("/tmp"),
                        category="dev",
                        description="List pull requests",
                        commit="def",
                    ),
                ]

        cfg = JarvisConfig()
        cfg.skills = SkillsConfig(sources=[SkillSourceConfig(source="hermes")])
        with patch("openjarvis.cli.skill_cmd.load_config", return_value=cfg):
            with patch(
                "openjarvis.cli.skill_cmd._get_resolver",
                return_value=_FakeResolver(),
            ):
                result = CliRunner().invoke(cli, ["skill", "search", "notes"])
                assert result.exit_code == 0, result.output
                assert "apple-notes" in result.output
                assert "github-prs" not in result.output


class TestSkillInstallCommand:
    def test_install_help(self) -> None:
        result = CliRunner().invoke(cli, ["skill", "install", "--help"])
        assert result.exit_code == 0
        assert "source" in result.output.lower()

    def test_install_invalid_source_format(self) -> None:
        result = CliRunner().invoke(cli, ["skill", "install", "invalid"])
        assert result.exit_code != 0
        assert "format" in result.output.lower() or "source" in result.output.lower()


class TestSkillSyncCommand:
    def test_sync_help(self) -> None:
        result = CliRunner().invoke(cli, ["skill", "sync", "--help"])
        assert result.exit_code == 0


class TestSkillSourcesCommand:
    def test_sources_help(self) -> None:
        result = CliRunner().invoke(cli, ["skill", "sources", "--help"])
        assert result.exit_code == 0

    def test_sources_lists_configured_sources(self, tmp_path: Path) -> None:
        from unittest.mock import patch

        from openjarvis.core.config import (
            JarvisConfig,
            SkillsConfig,
            SkillSourceConfig,
        )

        cfg = JarvisConfig()
        cfg.skills = SkillsConfig(
            sources=[
                SkillSourceConfig(source="hermes"),
                SkillSourceConfig(source="openclaw"),
            ]
        )
        with patch("openjarvis.cli.skill_cmd.load_config", return_value=cfg):
            result = CliRunner().invoke(cli, ["skill", "sources"])
            assert result.exit_code == 0
            assert "hermes" in result.output
            assert "openclaw" in result.output


class TestSkillDiscoverCommand:
    def test_discover_help(self) -> None:
        result = CliRunner().invoke(cli, ["skill", "discover", "--help"])
        assert result.exit_code == 0

    def test_discover_dry_run_no_traces(self, tmp_path: Path) -> None:
        from unittest.mock import patch

        # Patch the trace store to return nothing
        class _EmptyStore:
            def list_traces(self, *, limit: int = 100, **kwargs):
                return []

        with patch(
            "openjarvis.cli.skill_cmd._get_trace_store",
            return_value=_EmptyStore(),
        ):
            result = CliRunner().invoke(cli, ["skill", "discover", "--dry-run"])
            assert result.exit_code == 0

    def test_discover_writes_when_not_dry_run(self, tmp_path: Path) -> None:
        from unittest.mock import patch

        from openjarvis.core.types import StepType, Trace, TraceStep

        def _trace():
            return Trace(
                query="q",
                steps=[
                    TraceStep(
                        step_type=StepType.TOOL_CALL,
                        timestamp=0.0,
                        input={"tool": "web_search", "arguments": {}},
                        output={"success": True, "result": "ok"},
                        metadata={},
                    ),
                    TraceStep(
                        step_type=StepType.TOOL_CALL,
                        timestamp=0.0,
                        input={"tool": "calculator", "arguments": {}},
                        output={"success": True, "result": "ok"},
                        metadata={},
                    ),
                ],
                outcome="success",
                feedback=1.0,
            )

        class _Store:
            def list_traces(self, *, limit: int = 100, **kwargs):
                return [_trace() for _ in range(5)]

        output_dir = tmp_path / "discovered"
        with patch(
            "openjarvis.cli.skill_cmd._get_trace_store",
            return_value=_Store(),
        ):
            with patch(
                "openjarvis.cli.skill_cmd._get_discovered_dir",
                return_value=output_dir,
            ):
                result = CliRunner().invoke(
                    cli, ["skill", "discover", "--min-frequency", "3"]
                )
                assert result.exit_code == 0
                assert output_dir.exists()
                # At least one manifest should have been written
                assert any(output_dir.rglob("skill.toml"))


class TestSkillShowOverlayCommand:
    def test_show_overlay_help(self) -> None:
        result = CliRunner().invoke(cli, ["skill", "show-overlay", "--help"])
        assert result.exit_code == 0

    def test_show_overlay_missing(self, tmp_path: Path) -> None:
        from unittest.mock import patch

        with patch(
            "openjarvis.cli.skill_cmd._get_overlay_dir",
            return_value=tmp_path / "no-such",
        ):
            result = CliRunner().invoke(cli, ["skill", "show-overlay", "nonexistent"])
            assert result.exit_code != 0

    def test_show_overlay_displays_optimized(self, tmp_path: Path) -> None:
        from unittest.mock import patch

        from openjarvis.skills.overlay import SkillOverlay, write_overlay

        write_overlay(
            SkillOverlay(
                skill_name="research-skill",
                optimizer="dspy",
                optimized_at="2026-04-08T14:30:00Z",
                trace_count=42,
                description="Better description",
                few_shot=[{"input": "q", "output": "a"}],
            ),
            tmp_path,
        )

        with patch(
            "openjarvis.cli.skill_cmd._get_overlay_dir",
            return_value=tmp_path,
        ):
            result = CliRunner().invoke(
                cli, ["skill", "show-overlay", "research-skill"]
            )
            assert result.exit_code == 0
            assert "research-skill" in result.output
            assert "Better description" in result.output
            assert "42" in result.output
            assert "dspy" in result.output
