"""
Unit tests for marker_ops module.

Tests editable mode, shadow mode, and marker file operations.
"""

import subprocess
from pathlib import Path

import pytest

from persuade.marker_ops import (
    create_editable_marker,
    enable_shadow_mode,
    get_editable_metadata,
    is_editable_mode_enabled,
    is_shadow_mode_enabled,
    remove_editable_marker,
)

from .conftest import check_git_available, init_git_repo

# Skip git-related tests if git is not available
pytestmark = pytest.mark.skipif(not check_git_available(), reason="Git is not available")


class TestEditableMode:
    """Tests for editable mode functions."""

    def test_is_editable_mode_disabled_by_default(self, tmp_path: Path) -> None:
        """Test editable mode is disabled when no marker exists."""
        assert is_editable_mode_enabled(tmp_path) is False

    def test_create_and_check_editable_marker(self, tmp_path: Path) -> None:
        """Test creating editable marker file."""
        source_path = tmp_path / "source"
        source_path.mkdir()

        create_editable_marker(tmp_path, source_path)

        assert is_editable_mode_enabled(tmp_path) is True
        assert (tmp_path / ".claude" / ".editable").exists()

    def test_get_editable_metadata(self, tmp_path: Path) -> None:
        """Test reading editable metadata."""
        source_path = tmp_path / "source"
        source_path.mkdir()

        create_editable_marker(tmp_path, source_path)

        metadata = get_editable_metadata(tmp_path)
        assert metadata is not None
        assert "template_path" in metadata
        assert "enabled_at" in metadata
        assert metadata["mode"] == "editable"

    def test_remove_editable_marker(self, tmp_path: Path) -> None:
        """Test removing editable marker."""
        source_path = tmp_path / "source"
        source_path.mkdir()

        create_editable_marker(tmp_path, source_path)
        assert is_editable_mode_enabled(tmp_path) is True

        remove_editable_marker(tmp_path)
        assert is_editable_mode_enabled(tmp_path) is False


class TestShadowMode:
    """Tests for shadow mode functions."""

    def test_is_shadow_mode_disabled_by_default(self, tmp_path: Path) -> None:
        """Test shadow mode is disabled when no marker exists."""
        assert is_shadow_mode_enabled(tmp_path) is False

    def test_enable_shadow_mode_creates_marker(self, tmp_path: Path) -> None:
        """Test enabling shadow mode creates marker file."""
        init_git_repo(tmp_path)

        enable_shadow_mode(tmp_path)

        assert is_shadow_mode_enabled(tmp_path) is True
        assert (tmp_path / ".claude" / ".shadow").exists()

    def test_enable_shadow_mode_updates_gitignore(self, tmp_path: Path) -> None:
        """Test shadow mode adds entries to .git/info/exclude."""
        init_git_repo(tmp_path)

        additions, _ = enable_shadow_mode(tmp_path)

        assert len(additions) > 0
        assert ".claude/" in additions

        exclude_content = (tmp_path / ".git" / "info" / "exclude").read_text()
        assert ".claude/" in exclude_content

    def test_enable_shadow_mode_detects_staged_files(self, tmp_path: Path) -> None:
        """Test shadow mode detects staged .claude files (Issue #157 fix).

        This is the key test for Issue #157. When files are staged before
        enabling shadow mode, the returned `untrack_commands` should include
        a command to unstage them.
        """
        init_git_repo(tmp_path)
        (tmp_path / "README.md").write_text("test")
        subprocess.run(["git", "add", "."], cwd=tmp_path, capture_output=True, check=True)
        subprocess.run(
            ["git", "commit", "-m", "initial"],
            cwd=tmp_path,
            capture_output=True,
            check=True,
        )

        # Create .claude directory and stage a file
        claude_dir = tmp_path / ".claude"
        claude_dir.mkdir()
        (claude_dir / "settings.json").write_text("{}")
        subprocess.run(
            ["git", "add", ".claude/settings.json"],
            cwd=tmp_path,
            capture_output=True,
            check=True,
        )

        # Verify file is staged
        status_result = subprocess.run(
            ["git", "diff", "--cached", "--name-only"],
            cwd=tmp_path,
            capture_output=True,
            text=True,
            check=True,
        )
        assert ".claude/settings.json" in status_result.stdout

        # Now enable shadow mode - it should detect the staged file
        _, untrack_commands = enable_shadow_mode(tmp_path)

        # Should include unstage command for staged files
        assert any("restore --staged" in cmd for cmd in untrack_commands)

    def test_enable_shadow_mode_detects_tracked_files(self, tmp_path: Path) -> None:
        """Test shadow mode detects already committed .claude files."""
        init_git_repo(tmp_path)

        # Create and commit .claude file
        claude_dir = tmp_path / ".claude"
        claude_dir.mkdir()
        (claude_dir / "test.md").write_text("test")
        subprocess.run(["git", "add", "."], cwd=tmp_path, capture_output=True, check=True)
        subprocess.run(
            ["git", "commit", "-m", "initial"],
            cwd=tmp_path,
            capture_output=True,
            check=True,
        )

        # Enable shadow mode
        _, untrack_commands = enable_shadow_mode(tmp_path)

        # Should include untrack command for committed files
        assert any("rm -r --cached" in cmd for cmd in untrack_commands)
        assert any("commit" in cmd for cmd in untrack_commands)

    def test_enable_shadow_mode_no_untrack_for_clean_state(self, tmp_path: Path) -> None:
        """Test no untrack commands when no .claude files exist."""
        init_git_repo(tmp_path)
        (tmp_path / "README.md").write_text("test")
        subprocess.run(["git", "add", "."], cwd=tmp_path, capture_output=True, check=True)
        subprocess.run(
            ["git", "commit", "-m", "initial"],
            cwd=tmp_path,
            capture_output=True,
            check=True,
        )

        _, untrack_commands = enable_shadow_mode(tmp_path)

        # No untrack commands needed when no .claude files exist
        assert len(untrack_commands) == 0

    def test_enable_shadow_mode_idempotent_gitignore(self, tmp_path: Path) -> None:
        """Test calling enable_shadow_mode twice doesn't duplicate entries."""
        init_git_repo(tmp_path)

        # First call
        additions1, _ = enable_shadow_mode(tmp_path)
        assert len(additions1) > 0

        # Second call — block is replaced, not duplicated
        additions2, _ = enable_shadow_mode(tmp_path)
        assert len(additions2) > 0  # Entries are always re-written to replace the block

        # Verify no duplicates in .git/info/exclude
        exclude_content = (tmp_path / ".git" / "info" / "exclude").read_text()
        assert exclude_content.count(".claude/") == 1

    def test_enable_shadow_mode_writes_git_exclude(self, tmp_path: Path) -> None:
        """Test shadow mode writes to .git/info/exclude instead of .gitignore."""
        init_git_repo(tmp_path)

        synced_destinations = [
            ".claude/commands/test.md",
            ".github/workflows/ci.yml",
            "lefthook.yml",
        ]

        additions, _ = enable_shadow_mode(tmp_path, synced_destinations)

        # Should write to .git/info/exclude
        exclude_path = tmp_path / ".git" / "info" / "exclude"
        assert exclude_path.exists()
        content = exclude_path.read_text()
        assert "# persuade shadow mode" in content
        assert ".claude/" in content
        assert ".github/workflows/ci.yml" in content
        assert "lefthook.yml" in content
        assert "# end persuade" in content

        # .claude/ paths should NOT appear individually (covered by .claude/ dir entry)
        lines = content.splitlines()
        assert ".claude/commands/test.md" not in lines

    def test_enable_shadow_mode_replaces_block_on_resync(self, tmp_path: Path) -> None:
        """Test re-sync replaces the persuade block, doesn't duplicate."""
        init_git_repo(tmp_path)

        # First sync with one external file
        enable_shadow_mode(tmp_path, [".github/workflows/ci.yml"])

        # Second sync with different external file
        additions, _ = enable_shadow_mode(tmp_path, ["lefthook.yml"])

        exclude_content = (tmp_path / ".git" / "info" / "exclude").read_text()

        # Old file should be gone, new file should be present
        assert ".github/workflows/ci.yml" not in exclude_content
        assert "lefthook.yml" in exclude_content
        assert ".claude/" in exclude_content
        assert exclude_content.count("# persuade shadow mode") == 1

    def test_enable_shadow_mode_skips_exclude_without_git(self, tmp_path: Path) -> None:
        """Test shadow mode skips .git/info/exclude when no .git dir."""
        additions, _ = enable_shadow_mode(tmp_path, [".github/workflows/ci.yml"])

        assert is_shadow_mode_enabled(tmp_path)
        assert not (tmp_path / ".git" / "info" / "exclude").exists()
        assert len(additions) == 0

    def test_enable_shadow_mode_preserves_existing_excludes(self, tmp_path: Path) -> None:
        """Test shadow mode preserves pre-existing .git/info/exclude content."""
        init_git_repo(tmp_path)

        exclude_path = tmp_path / ".git" / "info" / "exclude"
        exclude_path.parent.mkdir(parents=True, exist_ok=True)
        exclude_path.write_text("# my custom excludes\n*.log\nbuild/\n")

        enable_shadow_mode(tmp_path, [".github/workflows/ci.yml"])

        content = exclude_path.read_text()
        assert "# my custom excludes" in content
        assert "*.log" in content
        assert "build/" in content
        assert "# persuade shadow mode" in content
        assert ".claude/" in content

    def test_enable_shadow_mode_preserves_content_after_block(self, tmp_path: Path) -> None:
        """Test content after the persuade block is preserved on re-sync."""
        init_git_repo(tmp_path)

        exclude_path = tmp_path / ".git" / "info" / "exclude"
        exclude_path.parent.mkdir(parents=True, exist_ok=True)

        # Write initial block with user content after it
        exclude_path.write_text(
            "# persuade shadow mode \u2014 do not edit this block\n"
            ".claude/\n"
            "# end persuade\n"
            "# user stuff after\n"
            "my-custom-exclude\n"
        )

        enable_shadow_mode(tmp_path, ["lefthook.yml"])

        content = exclude_path.read_text()
        assert "my-custom-exclude" in content
        assert "# user stuff after" in content
        assert "lefthook.yml" in content
        assert content.count("# persuade shadow mode") == 1

    def test_enable_shadow_mode_handles_malformed_block(self, tmp_path: Path) -> None:
        """Test malformed block (missing end marker) is handled gracefully."""
        init_git_repo(tmp_path)

        exclude_path = tmp_path / ".git" / "info" / "exclude"
        exclude_path.parent.mkdir(parents=True, exist_ok=True)

        # Write a block with start marker but no end marker
        exclude_path.write_text(
            "# before content\n"
            "*.log\n"
            "# persuade shadow mode \u2014 do not edit this block\n"
            ".claude/\n"
            "stale-entry\n"
        )

        enable_shadow_mode(tmp_path, ["lefthook.yml"])

        content = exclude_path.read_text()
        # Content before the malformed block should survive
        assert "*.log" in content
        # New block should be written correctly
        assert "# persuade shadow mode" in content
        assert "# end persuade" in content
        assert "lefthook.yml" in content
        # Stale content from malformed block should be gone
        assert "stale-entry" not in content

    def test_enable_shadow_mode_detects_tracked_external_files(self, tmp_path: Path) -> None:
        """Test shadow mode detects already-tracked external synced files."""
        init_git_repo(tmp_path)

        # Create and commit an external file
        workflows_dir = tmp_path / ".github" / "workflows"
        workflows_dir.mkdir(parents=True)
        (workflows_dir / "ci.yml").write_text("name: CI\n")
        subprocess.run(["git", "add", "."], cwd=tmp_path, capture_output=True, check=True)
        subprocess.run(
            ["git", "commit", "-m", "initial"],
            cwd=tmp_path,
            capture_output=True,
            check=True,
        )

        # Enable shadow mode with that file as a synced destination
        _, untrack_commands = enable_shadow_mode(tmp_path, [".github/workflows/ci.yml"])

        # Should include untrack command for the external file
        assert any(".github/workflows/ci.yml" in cmd for cmd in untrack_commands)

    def test_enable_shadow_mode_quotes_paths_in_untrack_commands(self, tmp_path: Path) -> None:
        """Test untrack commands use proper shell quoting for paths with spaces."""
        init_git_repo(tmp_path)

        # Create and commit a file with spaces in name
        dir_path = tmp_path / "my dir"
        dir_path.mkdir()
        (dir_path / "my file.yml").write_text("test\n")
        subprocess.run(["git", "add", "."], cwd=tmp_path, capture_output=True, check=True)
        subprocess.run(
            ["git", "commit", "-m", "initial"],
            cwd=tmp_path,
            capture_output=True,
            check=True,
        )

        _, untrack_commands = enable_shadow_mode(tmp_path, ["my dir/my file.yml"])

        # The path should be shell-quoted in the command
        matching = [cmd for cmd in untrack_commands if "my dir" in cmd]
        assert len(matching) > 0
        # shlex.quote wraps in single quotes for paths with spaces
        assert any("'" in cmd or '"' in cmd for cmd in matching)
