"""
Unit tests for CLI commands with optional target_path parameter.

Tests verify that sync, doctor, help-command, and rollback commands
work correctly both with and without explicit target_path argument,
and properly resolve to git repository root.
"""

import os
import subprocess
from pathlib import Path

import pytest
from click.testing import CliRunner

from persuade.cli import cli
from persuade.cli_sync import get_git_root


class TestGitRepositoryDetection:
    """Test git repository detection via get_git_root function."""

    def test_get_git_root_in_repo(self, tmp_path: Path) -> None:
        """Test get_git_root returns correct root for git repository."""
        # Initialize a real git repo for this test
        subprocess.run(["git", "init"], cwd=tmp_path, check=True, capture_output=True)

        root = get_git_root(tmp_path)
        assert root is not None
        assert root == tmp_path

    def test_get_git_root_in_subdirectory(self, tmp_path: Path) -> None:
        """Test get_git_root finds root from subdirectory."""
        # Initialize a real git repo
        subprocess.run(["git", "init"], cwd=tmp_path, check=True, capture_output=True)

        # Create a subdirectory
        subdir = tmp_path / "subdir" / "nested"
        subdir.mkdir(parents=True)

        root = get_git_root(subdir)
        assert root is not None
        assert root == tmp_path

    def test_get_git_root_not_in_repo(self, tmp_path: Path) -> None:
        """Test get_git_root returns None for non-git directory."""
        root = get_git_root(tmp_path)
        assert root is None


class TestCLIOptionalPath:
    """Test CLI commands with optional target_path parameter.

    Note: Uses shared fixtures from conftest.py:
    - runner: CliRunner instance
    - mock_git_project: Git-initialized project with .claude directory
    """

    @pytest.mark.parametrize(
        "command,extra_args",
        [
            (["sync"], ["--dry-run"]),
            (["doctor"], ["--check-only"]),
            (["help-command"], []),
            (["rollback"], ["--list"]),
        ],
        ids=["sync", "doctor", "help-command", "rollback"],
    )
    def test_command_with_explicit_path(
        self,
        runner: CliRunner,
        mock_git_project: Path,
        command: list[str],
        extra_args: list[str],
    ) -> None:
        """Test CLI commands with explicit target_path argument."""
        result = runner.invoke(cli, command + [str(mock_git_project)] + extra_args)

        # If command shows help, argument parsing failed
        if "--help" in result.output.lower():
            pytest.fail("Command showed help instead of executing - argument not accepted")

        # Otherwise verify exit code is reasonable (0=success, 1=business logic error)
        assert result.exit_code in [
            0,
            1,
        ], f"Unexpected exit code: {result.exit_code}. Output: {result.output[:500]}"

    @pytest.mark.parametrize(
        "command,extra_args",
        [
            (["sync"], ["--dry-run"]),
            (["doctor"], ["--check-only"]),
            (["help-command"], []),
            (["rollback"], ["--list"]),
        ],
        ids=["sync", "doctor", "help-command", "rollback"],
    )
    def test_command_without_path_uses_cwd(
        self,
        runner: CliRunner,
        mock_git_project: Path,
        monkeypatch: pytest.MonkeyPatch,
        command: list[str],
        extra_args: list[str],
    ) -> None:
        """Test CLI commands default to current directory when path not provided."""
        monkeypatch.chdir(mock_git_project)

        result = runner.invoke(cli, command + extra_args)

        # If command shows help, argument parsing failed
        if "--help" in result.output.lower():
            pytest.fail("Command showed help instead of executing - should default to cwd")

        # Otherwise verify exit code is reasonable (0=success, 1=business logic error)
        assert result.exit_code in [
            0,
            1,
        ], f"Unexpected exit code: {result.exit_code}. Output: {result.output[:500]}"


class TestCLIHelpText:
    """Test CLI help text properly documents optional parameters.

    Note: Uses shared runner fixture from conftest.py.
    """

    @pytest.mark.parametrize(
        "command",
        ["sync", "doctor", "help-command", "rollback"],
    )
    def test_help_shows_optional_path(self, runner: CliRunner, command: str) -> None:
        """Test that command --help shows target_path as optional."""
        result = runner.invoke(cli, [command, "--help"])

        assert result.exit_code == 0
        # Check that help text mentions optional path or default directory
        output_lower = result.output.lower()
        path_keywords = ["target_path", "target path", "git repository root"]
        has_path_docs = any(kw in output_lower for kw in path_keywords)
        assert has_path_docs, f"{command} help should document optional target_path"


class TestCLIPathValidation:
    """Test path validation and error handling.

    Note: Uses shared runner fixture from conftest.py.
    """

    def test_sync_rejects_nonexistent_explicit_path(
        self,
        runner: CliRunner,
        tmp_path: Path,
    ) -> None:
        """Test that sync rejects explicit path that doesn't exist."""
        nonexistent = tmp_path / "does_not_exist"

        result = runner.invoke(cli, ["sync", str(nonexistent), "--dry-run"])

        # Should fail with error about path not existing
        assert result.exit_code != 0, "Should reject nonexistent path"
        output_lower = result.output.lower()
        error_keywords = ["does not exist", "not found", "invalid"]
        has_error = any(kw in output_lower for kw in error_keywords)
        assert has_error, "Should show error about nonexistent path"

    def test_doctor_rejects_nonexistent_explicit_path(
        self,
        runner: CliRunner,
        tmp_path: Path,
    ) -> None:
        """Test that doctor rejects explicit path that doesn't exist."""
        nonexistent = tmp_path / "does_not_exist"

        result = runner.invoke(cli, ["doctor", str(nonexistent), "--check-only"])

        # Should fail with error about path not existing
        assert result.exit_code != 0, "Should reject nonexistent path"

    def test_sync_rejects_file_path(self, runner: CliRunner, tmp_path: Path) -> None:
        """Test that sync rejects file path (requires directory)."""
        file_path = tmp_path / "test.txt"
        file_path.write_text("test")

        result = runner.invoke(cli, ["sync", str(file_path), "--dry-run"])

        # Should fail because path must be a directory
        assert result.exit_code != 0, "Should reject file path"
        output_lower = result.output.lower()
        has_dir_error = "directory" in output_lower or "invalid" in output_lower
        assert has_dir_error, "Should indicate path must be directory"


class TestCLIBackwardCompatibility:
    """Test that existing usage patterns continue to work.

    Note: Uses shared fixtures from conftest.py:
    - runner: CliRunner instance
    - mock_git_project: Git-initialized project with .claude directory
    """

    @pytest.mark.parametrize(
        "command,extra_args",
        [
            (["sync"], ["--dry-run"]),
            (["doctor"], ["--check-only"]),
            (["rollback"], ["--list"]),
        ],
        ids=["sync", "doctor", "rollback"],
    )
    def test_existing_usage_still_works(
        self,
        runner: CliRunner,
        mock_git_project: Path,
        command: list[str],
        extra_args: list[str],
    ) -> None:
        """Test that existing persuade command <path> usage still works."""
        # This is the existing usage pattern that must remain functional
        result = runner.invoke(cli, command + [str(mock_git_project)] + extra_args)

        # Should work (backward compatible): 0=success, 1=business logic error, not 2=argument error
        assert result.exit_code in [
            0,
            1,
        ], f"Backward compat failed - exit {result.exit_code}. Output: {result.output[:500]}"


class TestGitRepositoryValidation:
    """Test git repository validation for CLI commands.

    Note: Uses shared fixtures from conftest.py:
    - runner: CliRunner instance
    - mock_git_project: Git-initialized project
    - non_git_project: Directory without git initialization
    """

    def test_sync_rejects_non_git_directory(
        self,
        runner: CliRunner,
        non_git_project: Path,
    ) -> None:
        """Test that sync rejects non-git directories by default."""
        result = runner.invoke(cli, ["sync", str(non_git_project), "--dry-run"])

        assert result.exit_code != 0
        # Check for git-related error (flexible matching for message variations)
        assert "git" in result.output.lower() and "repository" in result.output.lower()

    def test_sync_allows_non_git_with_flag(
        self,
        runner: CliRunner,
        non_git_project: Path,
    ) -> None:
        """Test that sync allows non-git directories with --allow-non-git flag."""
        result = runner.invoke(cli, ["sync", str(non_git_project), "--dry-run", "--allow-non-git"])

        # Should not fail due to git validation (may fail for other reasons)
        assert "not inside a git repository" not in result.output
        # Verify command attempted to execute (not just show help/usage)
        assert result.exit_code in [
            0,
            1,
        ], f"Unexpected exit code: {result.exit_code}. Output: {result.output[:500]}"
        if "usage:" in result.output.lower() or "--help" in result.output.lower():
            pytest.fail("Command showed usage/help instead of executing")

    def test_sync_accepts_git_directory(
        self,
        runner: CliRunner,
        mock_git_project: Path,
    ) -> None:
        """Test that sync accepts git directories without flag."""
        result = runner.invoke(cli, ["sync", str(mock_git_project), "--dry-run"])

        # Should not fail due to git validation
        assert "not inside a git repository" not in result.output

    def test_doctor_rejects_non_git_directory(
        self,
        runner: CliRunner,
        non_git_project: Path,
    ) -> None:
        """Test that doctor rejects non-git directories by default."""
        result = runner.invoke(cli, ["doctor", str(non_git_project), "--check-only"])

        assert result.exit_code != 0
        assert "git" in result.output.lower() and "repository" in result.output.lower()

    def test_doctor_allows_non_git_with_flag(
        self,
        runner: CliRunner,
        non_git_project: Path,
    ) -> None:
        """Test that doctor allows non-git directories with --allow-non-git flag."""
        result = runner.invoke(
            cli, ["doctor", str(non_git_project), "--check-only", "--allow-non-git"]
        )

        # Should not fail due to git validation
        assert "not inside a git repository" not in result.output

    def test_help_command_rejects_non_git_directory(
        self,
        runner: CliRunner,
        non_git_project: Path,
    ) -> None:
        """Test that help-command rejects non-git directories by default."""
        result = runner.invoke(cli, ["help-command", str(non_git_project)])

        assert result.exit_code != 0
        assert "git" in result.output.lower() and "repository" in result.output.lower()

    def test_help_command_allows_non_git_with_flag(
        self,
        runner: CliRunner,
        non_git_project: Path,
    ) -> None:
        """Test that help-command allows non-git with --allow-non-git flag."""
        result = runner.invoke(cli, ["help-command", str(non_git_project), "--allow-non-git"])

        # Should not fail due to git validation
        assert "not inside a git repository" not in result.output

    def test_rollback_rejects_non_git_directory(
        self,
        runner: CliRunner,
        non_git_project: Path,
    ) -> None:
        """Test that rollback rejects non-git directories by default."""
        result = runner.invoke(cli, ["rollback", str(non_git_project), "--list"])

        assert result.exit_code != 0
        assert "git" in result.output.lower() and "repository" in result.output.lower()

    def test_rollback_allows_non_git_with_flag(
        self,
        runner: CliRunner,
        non_git_project: Path,
    ) -> None:
        """Test that rollback allows non-git directories with --allow-non-git flag."""
        result = runner.invoke(cli, ["rollback", str(non_git_project), "--list", "--allow-non-git"])

        # Should not fail due to git validation
        assert "not inside a git repository" not in result.output


class TestIteratePROptionalPath:
    """Test iterate-pr command with optional project_dir parameter and git resolution.

    Note: Uses shared fixtures from conftest.py:
    - runner: CliRunner instance
    - mock_git_project: Git-initialized project
    - non_git_project: Directory without git initialization
    """

    def test_iterate_pr_help_shows_git_resolution(self, runner: CliRunner) -> None:
        """Test that iterate-pr --help documents git repository root default."""
        result = runner.invoke(cli, ["iterate-pr", "--help"])

        assert result.exit_code == 0
        output_lower = result.output.lower()
        project_keywords = ["git repository root", "project-dir"]
        has_project_docs = any(kw in output_lower for kw in project_keywords)
        assert has_project_docs, "Help should document project directory option"

    def test_iterate_pr_help_shows_allow_non_git_flag(self, runner: CliRunner) -> None:
        """Test that iterate-pr --help shows --allow-non-git option."""
        result = runner.invoke(cli, ["iterate-pr", "--help"])

        assert result.exit_code == 0
        assert "--allow-non-git" in result.output

    def test_iterate_pr_help_shows_no_resolve_root_flag(self, runner: CliRunner) -> None:
        """Test that iterate-pr --help shows --no-resolve-root option."""
        result = runner.invoke(cli, ["iterate-pr", "--help"])

        assert result.exit_code == 0
        assert "--no-resolve-root" in result.output

    def test_iterate_pr_rejects_non_git_directory(
        self,
        runner: CliRunner,
        non_git_project: Path,
    ) -> None:
        """Test that iterate-pr rejects non-git directories by default."""
        result = runner.invoke(cli, ["iterate-pr", "123", "--project-dir", str(non_git_project)])

        assert result.exit_code != 0
        assert "git" in result.output.lower() and "repository" in result.output.lower()

    def test_iterate_pr_allows_non_git_with_flag(
        self,
        runner: CliRunner,
        non_git_project: Path,
    ) -> None:
        """Test that iterate-pr allows non-git directories with --allow-non-git flag."""
        result = runner.invoke(
            cli,
            [
                "iterate-pr",
                "123",
                "--project-dir",
                str(non_git_project),
                "--allow-non-git",
            ],
        )

        # Should not fail due to git validation (may fail for other reasons like missing deps)
        assert "not inside a git repository" not in result.output

    def test_iterate_pr_accepts_git_directory(
        self,
        runner: CliRunner,
        mock_git_project: Path,
    ) -> None:
        """Test that iterate-pr accepts git directories without flag."""
        result = runner.invoke(cli, ["iterate-pr", "123", "--project-dir", str(mock_git_project)])

        # Should not fail due to git validation (may fail for other reasons)
        assert "not inside a git repository" not in result.output

    def test_iterate_pr_no_resolve_root_uses_specified_path(
        self,
        runner: CliRunner,
        git_project_with_subdir: tuple[Path, Path],
    ) -> None:
        """Test that iterate-pr --no-resolve-root prevents auto-resolution to git root."""
        git_root, subdir = git_project_with_subdir

        result = runner.invoke(
            cli,
            [
                "iterate-pr",
                "123",
                "--project-dir",
                str(subdir),
                "--no-resolve-root",
            ],
        )

        # Should not fail due to path resolution
        # May fail for other reasons (e.g., missing dependencies)
        assert "--help" not in result.output.lower()
        # Should not show git validation error since subdir is inside git repo
        assert "not inside a git repository" not in result.output


class TestGitRootResolution:
    """Test automatic resolution to git repository root.

    Note: Uses shared fixtures from conftest.py:
    - runner: CliRunner instance
    - git_project_with_subdir: Tuple of (git_root, subdirectory)
    """

    def test_sync_resolves_to_git_root_from_subdirectory(
        self,
        runner: CliRunner,
        git_project_with_subdir: tuple[Path, Path],
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        """Test that sync resolves to git root when run from subdirectory."""
        git_root, subdir = git_project_with_subdir
        monkeypatch.chdir(subdir)

        result = runner.invoke(cli, ["sync", "--dry-run"])

        # Should mention using git root or succeed
        output_lower = result.output.lower()
        mentions_git_root = "git root" in output_lower or str(git_root) in result.output
        succeeded = result.exit_code in [0, 1]  # Success or business logic error
        assert mentions_git_root or succeeded

    def test_sync_no_resolve_root_uses_specified_path(
        self,
        runner: CliRunner,
        git_project_with_subdir: tuple[Path, Path],
    ) -> None:
        """Test that --no-resolve-root prevents auto-resolution to git root."""
        git_root, subdir = git_project_with_subdir

        result = runner.invoke(cli, ["sync", str(subdir), "--dry-run", "--no-resolve-root"])

        # Should not fail due to path resolution
        # May fail for other reasons (e.g., no .claude in subdirectory)
        assert "--help" not in result.output.lower()

    def test_doctor_resolves_to_git_root_from_subdirectory(
        self,
        runner: CliRunner,
        git_project_with_subdir: tuple[Path, Path],
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        """Test that doctor resolves to git root when run from subdirectory."""
        git_root, subdir = git_project_with_subdir
        monkeypatch.chdir(subdir)

        result = runner.invoke(cli, ["doctor", "--check-only"])

        # Should mention using git root or succeed
        output_lower = result.output.lower()
        mentions_git_root = "git root" in output_lower or str(git_root) in result.output
        succeeded = result.exit_code in [0, 1]
        assert mentions_git_root or succeeded

    def test_help_command_resolves_to_git_root(
        self,
        runner: CliRunner,
        git_project_with_subdir: tuple[Path, Path],
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        """Test that help-command resolves to git root from subdirectory."""
        git_root, subdir = git_project_with_subdir
        monkeypatch.chdir(subdir)

        result = runner.invoke(cli, ["help-command"])

        # Should mention using git root or succeed
        output_lower = result.output.lower()
        mentions_git_root = "git root" in output_lower or str(git_root) in result.output
        succeeded = result.exit_code in [0, 1]
        assert mentions_git_root or succeeded

    def test_rollback_resolves_to_git_root(
        self,
        runner: CliRunner,
        git_project_with_subdir: tuple[Path, Path],
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        """Test that rollback resolves to git root from subdirectory."""
        git_root, subdir = git_project_with_subdir
        monkeypatch.chdir(subdir)

        result = runner.invoke(cli, ["rollback", "--list"])

        # Should mention using git root or succeed
        output_lower = result.output.lower()
        mentions_git_root = "git root" in output_lower or str(git_root) in result.output
        succeeded = result.exit_code in [0, 1]
        assert mentions_git_root or succeeded


class TestEdgeCases:
    """Test edge cases for path handling.

    Tests cover:
    - Symlinked directories
    - Paths with special characters
    - Very deep nested directories

    Note: Uses shared fixtures from conftest.py.
    """

    def test_sync_handles_symlinked_directory(
        self,
        runner: CliRunner,
        mock_git_project: Path,
        tmp_path: Path,
    ) -> None:
        """Test that sync handles symlinked directories correctly."""
        # Create a symlink to the git project
        symlink_path = tmp_path / "project_symlink"
        try:
            symlink_path.symlink_to(mock_git_project)
        except OSError:
            pytest.skip("Symlinks not supported on this system")

        result = runner.invoke(cli, ["sync", str(symlink_path), "--dry-run"])

        # Should work with symlinked paths
        assert result.exit_code in [
            0,
            1,
        ], f"Unexpected exit code: {result.exit_code}. Output: {result.output[:500]}"
        # Should not show argument parsing errors
        assert "--help" not in result.output.lower()

    def test_sync_handles_path_with_spaces(
        self,
        runner: CliRunner,
        tmp_path: Path,
    ) -> None:
        """Test that sync handles paths with spaces correctly."""
        # Create a directory with spaces in the name
        project_path = tmp_path / "my project with spaces"
        project_path.mkdir()
        subprocess.run(["git", "init"], cwd=project_path, check=True, capture_output=True)

        claude_dir = project_path / ".claude"
        claude_dir.mkdir()
        (claude_dir / "settings.json").write_text("{}")

        result = runner.invoke(cli, ["sync", str(project_path), "--dry-run"])

        # Should handle paths with spaces
        assert result.exit_code in [
            0,
            1,
        ], f"Unexpected exit code: {result.exit_code}. Output: {result.output[:500]}"
        assert "--help" not in result.output.lower()

    def test_sync_handles_path_with_special_characters(
        self,
        runner: CliRunner,
        tmp_path: Path,
    ) -> None:
        """Test that sync handles paths with special characters correctly."""
        # Create a directory with special characters (avoiding shell-sensitive ones)
        project_path = tmp_path / "project-with_special.chars"
        project_path.mkdir()
        subprocess.run(["git", "init"], cwd=project_path, check=True, capture_output=True)

        claude_dir = project_path / ".claude"
        claude_dir.mkdir()
        (claude_dir / "settings.json").write_text("{}")

        result = runner.invoke(cli, ["sync", str(project_path), "--dry-run"])

        # Should handle paths with special characters
        assert result.exit_code in [
            0,
            1,
        ], f"Unexpected exit code: {result.exit_code}. Output: {result.output[:500]}"
        assert "--help" not in result.output.lower()

    def test_sync_handles_deeply_nested_subdirectory(
        self,
        runner: CliRunner,
        tmp_path: Path,
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        """Test that sync handles very deep nested directories correctly."""
        # Create a git repo
        subprocess.run(["git", "init"], cwd=tmp_path, check=True, capture_output=True)

        claude_dir = tmp_path / ".claude"
        claude_dir.mkdir()
        (claude_dir / "settings.json").write_text("{}")

        # Create a deeply nested subdirectory (10 levels deep)
        deep_path = tmp_path
        for i in range(10):
            deep_path = deep_path / f"level{i}"
        deep_path.mkdir(parents=True)

        monkeypatch.chdir(deep_path)

        result = runner.invoke(cli, ["sync", "--dry-run"])

        # Should resolve to git root from deep subdirectory
        output_lower = result.output.lower()
        mentions_git_root = "git root" in output_lower or str(tmp_path) in result.output
        succeeded = result.exit_code in [0, 1]
        assert mentions_git_root or succeeded

    @pytest.mark.skipif(
        os.name == "nt",
        reason="Unicode path test may not work on all Windows systems",
    )
    def test_sync_handles_unicode_path(
        self,
        runner: CliRunner,
        tmp_path: Path,
    ) -> None:
        """Test that sync handles paths with unicode characters correctly."""
        # Create a directory with unicode characters
        project_path = tmp_path / "проект"  # Russian word for "project"
        try:
            project_path.mkdir()
        except OSError:
            pytest.skip("Unicode paths not fully supported on this system")

        subprocess.run(["git", "init"], cwd=project_path, check=True, capture_output=True)

        claude_dir = project_path / ".claude"
        claude_dir.mkdir()
        (claude_dir / "settings.json").write_text("{}")

        result = runner.invoke(cli, ["sync", str(project_path), "--dry-run"])

        # Should handle unicode paths
        assert result.exit_code in [
            0,
            1,
        ], f"Unexpected exit code: {result.exit_code}. Output: {result.output[:500]}"
        assert "--help" not in result.output.lower()


if __name__ == "__main__":
    # Allow running tests directly
    pytest.main([__file__, "-v"])
