"""Tests for ksm.commands.sync module."""

import argparse
import sys
from datetime import datetime, timezone
from pathlib import Path
from unittest.mock import patch

import pytest
from hypothesis import given, HealthCheck, settings as h_settings
from hypothesis import strategies as st

from ksm.manifest import Manifest, ManifestEntry, save_manifest
from ksm.registry import RegistryEntry, RegistryIndex


def _setup_bundle(
    reg: Path,
    name: str,
    files: dict[str, bytes],
) -> None:
    """Create bundle files in a registry directory."""
    for rel, content in files.items():
        fpath = reg / name / rel
        fpath.parent.mkdir(parents=True, exist_ok=True)
        fpath.write_bytes(content)


def _make_entry(
    name: str = "aws",
    scope: str = "local",
    source: str = "default",
    files: list[str] | None = None,
) -> ManifestEntry:
    return ManifestEntry(
        bundle_name=name,
        source_registry=source,
        scope=scope,
        installed_files=files or ["skills/f.md"],
        installed_at="2025-01-01T00:00:00Z",
        updated_at="2025-01-01T00:00:00Z",
    )


def _make_args(**kwargs: object) -> argparse.Namespace:
    defaults: dict[str, object] = {
        "bundle_names": [],
        "all": False,
        "yes": False,
        "dry_run": False,
    }
    defaults.update(kwargs)
    return argparse.Namespace(**defaults)


def test_sync_aborts_on_non_y_input(
    tmp_path: Path,
    capsys: pytest.CaptureFixture[str],
) -> None:
    """Sync aborts when user responds with non-y input."""
    from ksm.commands.sync import run_sync

    reg = tmp_path / "reg"
    _setup_bundle(reg, "aws", {"skills/f.md": b"data"})
    target = tmp_path / "target" / ".kiro"
    target.mkdir(parents=True)
    (target / "skills").mkdir()
    (target / "skills" / "f.md").write_bytes(b"old")

    ksm_dir = tmp_path / "ksm"
    ksm_dir.mkdir(parents=True)

    idx = RegistryIndex(
        registries=[
            RegistryEntry(
                name="default",
                url=None,
                local_path=str(reg),
                is_default=True,
            )
        ]
    )
    manifest = Manifest(entries=[_make_entry("aws")])
    save_manifest(manifest, ksm_dir / "manifest.json")

    args = _make_args(bundle_names=["aws"])

    with (
        patch("builtins.input", return_value="n"),
        patch("sys.stdin") as mock_stdin,
    ):
        mock_stdin.isatty.return_value = True
        code = run_sync(
            args,
            registry_index=idx,
            manifest=manifest,
            manifest_path=ksm_dir / "manifest.json",
            target_local=target,
            target_global=tmp_path / "global" / ".kiro",
        )

    assert code == 0
    # File should remain unchanged
    assert (target / "skills" / "f.md").read_bytes() == b"old"


def test_sync_yes_skips_confirmation(tmp_path: Path) -> None:
    """Sync with --yes skips confirmation prompt."""
    from ksm.commands.sync import run_sync

    reg = tmp_path / "reg"
    _setup_bundle(reg, "aws", {"skills/f.md": b"new"})
    target = tmp_path / "target" / ".kiro"
    target.mkdir(parents=True)
    (target / "skills").mkdir()
    (target / "skills" / "f.md").write_bytes(b"old")

    ksm_dir = tmp_path / "ksm"
    ksm_dir.mkdir(parents=True)

    idx = RegistryIndex(
        registries=[
            RegistryEntry(
                name="default",
                url=None,
                local_path=str(reg),
                is_default=True,
            )
        ]
    )
    manifest = Manifest(entries=[_make_entry("aws", files=["skills/f.md"])])
    save_manifest(manifest, ksm_dir / "manifest.json")

    args = _make_args(bundle_names=["aws"], yes=True)
    code = run_sync(
        args,
        registry_index=idx,
        manifest=manifest,
        manifest_path=ksm_dir / "manifest.json",
        target_local=target,
        target_global=tmp_path / "global" / ".kiro",
    )

    assert code == 0
    assert (target / "skills" / "f.md").read_bytes() == b"new"


def test_sync_recopies_files(tmp_path: Path) -> None:
    """Sync re-copies files from source registry."""
    from ksm.commands.sync import run_sync

    reg = tmp_path / "reg"
    _setup_bundle(reg, "aws", {"skills/f.md": b"updated"})
    target = tmp_path / "target" / ".kiro"
    target.mkdir(parents=True)
    (target / "skills").mkdir()
    (target / "skills" / "f.md").write_bytes(b"stale")

    ksm_dir = tmp_path / "ksm"
    ksm_dir.mkdir(parents=True)

    idx = RegistryIndex(
        registries=[
            RegistryEntry(
                name="default",
                url=None,
                local_path=str(reg),
                is_default=True,
            )
        ]
    )
    manifest = Manifest(entries=[_make_entry("aws", files=["skills/f.md"])])

    args = _make_args(bundle_names=["aws"], yes=True)
    code = run_sync(
        args,
        registry_index=idx,
        manifest=manifest,
        manifest_path=ksm_dir / "manifest.json",
        target_local=target,
        target_global=tmp_path / "global" / ".kiro",
    )

    assert code == 0
    assert (target / "skills" / "f.md").read_bytes() == b"updated"


def test_sync_all_syncs_all_bundles(tmp_path: Path) -> None:
    """Sync with --all syncs all installed bundles."""
    from ksm.commands.sync import run_sync

    reg = tmp_path / "reg"
    _setup_bundle(reg, "aws", {"skills/f.md": b"aws-new"})
    _setup_bundle(reg, "team", {"steering/s.md": b"team-new"})
    target = tmp_path / "target" / ".kiro"
    target.mkdir(parents=True)
    (target / "skills").mkdir()
    (target / "skills" / "f.md").write_bytes(b"aws-old")
    (target / "steering").mkdir()
    (target / "steering" / "s.md").write_bytes(b"team-old")

    ksm_dir = tmp_path / "ksm"
    ksm_dir.mkdir(parents=True)

    idx = RegistryIndex(
        registries=[
            RegistryEntry(
                name="default",
                url=None,
                local_path=str(reg),
                is_default=True,
            )
        ]
    )
    manifest = Manifest(
        entries=[
            _make_entry("aws", files=["skills/f.md"]),
            _make_entry("team", files=["steering/s.md"]),
        ]
    )

    args = _make_args(all=True, yes=True)
    code = run_sync(
        args,
        registry_index=idx,
        manifest=manifest,
        manifest_path=ksm_dir / "manifest.json",
        target_local=target,
        target_global=tmp_path / "global" / ".kiro",
    )

    assert code == 0
    assert (target / "skills" / "f.md").read_bytes() == b"aws-new"
    assert (target / "steering" / "s.md").read_bytes() == b"team-new"


def test_sync_unknown_bundle_prints_error_continues(
    tmp_path: Path,
    capsys: pytest.CaptureFixture[str],
) -> None:
    """Sync with unknown bundle prints error and continues."""
    from ksm.commands.sync import run_sync

    reg = tmp_path / "reg"
    _setup_bundle(reg, "aws", {"skills/f.md": b"new"})
    target = tmp_path / "target" / ".kiro"
    target.mkdir(parents=True)
    (target / "skills").mkdir()
    (target / "skills" / "f.md").write_bytes(b"old")

    ksm_dir = tmp_path / "ksm"
    ksm_dir.mkdir(parents=True)

    idx = RegistryIndex(
        registries=[
            RegistryEntry(
                name="default",
                url=None,
                local_path=str(reg),
                is_default=True,
            )
        ]
    )
    manifest = Manifest(entries=[_make_entry("aws", files=["skills/f.md"])])

    args = _make_args(bundle_names=["nonexistent", "aws"], yes=True)
    code = run_sync(
        args,
        registry_index=idx,
        manifest=manifest,
        manifest_path=ksm_dir / "manifest.json",
        target_local=target,
        target_global=tmp_path / "global" / ".kiro",
    )

    assert code == 0
    captured = capsys.readouterr()
    assert "nonexistent" in captured.err
    assert (target / "skills" / "f.md").read_bytes() == b"new"


def test_sync_updates_manifest_timestamp(
    tmp_path: Path,
) -> None:
    """Sync updates manifest updated_at timestamp."""
    from ksm.commands.sync import run_sync

    reg = tmp_path / "reg"
    _setup_bundle(reg, "aws", {"skills/f.md": b"data"})
    target = tmp_path / "target" / ".kiro"
    ksm_dir = tmp_path / "ksm"
    ksm_dir.mkdir(parents=True)

    idx = RegistryIndex(
        registries=[
            RegistryEntry(
                name="default",
                url=None,
                local_path=str(reg),
                is_default=True,
            )
        ]
    )
    old_ts = "2020-01-01T00:00:00Z"
    manifest = Manifest(
        entries=[
            _make_entry("aws", files=["skills/f.md"]),
        ]
    )
    manifest.entries[0].updated_at = old_ts

    args = _make_args(bundle_names=["aws"], yes=True)
    before = datetime.now(timezone.utc)
    code = run_sync(
        args,
        registry_index=idx,
        manifest=manifest,
        manifest_path=ksm_dir / "manifest.json",
        target_local=target,
        target_global=tmp_path / "global" / ".kiro",
    )

    assert code == 0
    assert manifest.entries[0].updated_at != old_ts
    updated = datetime.fromisoformat(manifest.entries[0].updated_at)
    assert updated >= before


def test_sync_no_args_no_all_prints_usage(
    tmp_path: Path,
    capsys: pytest.CaptureFixture[str],
) -> None:
    """Sync with no args and no --all prints usage message."""
    from ksm.commands.sync import run_sync

    ksm_dir = tmp_path / "ksm"
    ksm_dir.mkdir(parents=True)

    idx = RegistryIndex(registries=[])
    manifest = Manifest(entries=[])

    args = _make_args()
    code = run_sync(
        args,
        registry_index=idx,
        manifest=manifest,
        manifest_path=ksm_dir / "manifest.json",
        target_local=tmp_path / "local" / ".kiro",
        target_global=tmp_path / "global" / ".kiro",
    )

    assert code == 1
    captured = capsys.readouterr()
    assert "usage" in captured.err.lower() or "--all" in captured.err


def test_sync_git_pull_called_for_custom_registries(
    tmp_path: Path,
) -> None:
    """Sync calls git pull for custom (non-default) registries."""
    from ksm.commands.sync import run_sync

    reg = tmp_path / "reg"
    _setup_bundle(reg, "team", {"skills/f.md": b"data"})
    target = tmp_path / "target" / ".kiro"
    ksm_dir = tmp_path / "ksm"
    ksm_dir.mkdir(parents=True)

    idx = RegistryIndex(
        registries=[
            RegistryEntry(
                name="team-configs",
                url="https://github.com/org/team.git",
                local_path=str(reg),
                is_default=False,
            )
        ]
    )
    manifest = Manifest(
        entries=[
            _make_entry(
                "team",
                source="team-configs",
                files=["skills/f.md"],
            ),
        ]
    )

    args = _make_args(bundle_names=["team"], yes=True)

    with patch("ksm.commands.sync.pull_repo") as mock_pull:
        code = run_sync(
            args,
            registry_index=idx,
            manifest=manifest,
            manifest_path=ksm_dir / "manifest.json",
            target_local=target,
            target_global=tmp_path / "global" / ".kiro",
        )

    assert code == 0
    mock_pull.assert_called_once_with(Path(str(reg)))


def test_sync_empty_entries_returns_zero(
    tmp_path: Path,
) -> None:
    """Sync with named bundles that all match returns 0 normally,
    but if entries_to_sync ends up empty it returns 0 early."""
    from ksm.commands.sync import run_sync

    ksm_dir = tmp_path / "ksm"
    ksm_dir.mkdir()
    reg = tmp_path / "registries" / "default"
    reg.mkdir(parents=True)

    idx = RegistryIndex(
        registries=[
            RegistryEntry(
                name="default",
                url=None,
                local_path=str(reg),
                is_default=True,
            )
        ]
    )
    # Manifest has an entry but we pass a name that doesn't match
    manifest = Manifest(entries=[_make_entry("aws", "local", "default")])
    # Pass bundle name that doesn't match — it prints error
    # and continues, leaving entries_to_sync empty
    args = _make_args(bundle_names=["nonexistent"])
    code = run_sync(
        args,
        registry_index=idx,
        manifest=manifest,
        manifest_path=ksm_dir / "manifest.json",
        target_local=tmp_path / "local",
        target_global=tmp_path / "global",
    )

    assert code == 0


def test_sync_eoferror_aborts(
    tmp_path: Path,
) -> None:
    """Sync aborts when input() raises EOFError."""
    from ksm.commands.sync import run_sync

    ksm_dir = tmp_path / "ksm"
    ksm_dir.mkdir()
    reg = tmp_path / "registries" / "default"
    _setup_bundle(reg, "aws", {"skills/f.md": b"data"})

    idx = RegistryIndex(
        registries=[
            RegistryEntry(
                name="default",
                url=None,
                local_path=str(reg),
                is_default=True,
            )
        ]
    )
    manifest = Manifest(entries=[_make_entry("aws", "local", "default")])
    args = _make_args(bundle_names=["aws"])

    with (
        patch("builtins.input", side_effect=EOFError),
        patch("sys.stdin") as mock_stdin,
    ):
        mock_stdin.isatty.return_value = True
        code = run_sync(
            args,
            registry_index=idx,
            manifest=manifest,
            manifest_path=ksm_dir / "manifest.json",
            target_local=tmp_path / "local",
            target_global=tmp_path / "global",
        )

    assert code == 0


def test_sync_pull_repo_failure_continues(
    tmp_path: Path,
    capsys: pytest.CaptureFixture[str],
) -> None:
    """Sync continues when pull_repo raises an exception."""
    from ksm.commands.sync import run_sync

    ksm_dir = tmp_path / "ksm"
    ksm_dir.mkdir()
    reg = tmp_path / "registries" / "custom"
    _setup_bundle(reg, "team", {"skills/f.md": b"data"})

    target = tmp_path / "workspace" / ".kiro"
    (target / "skills").mkdir(parents=True)
    (target / "skills" / "f.md").write_bytes(b"old")

    idx = RegistryIndex(
        registries=[
            RegistryEntry(
                name="custom",
                url="https://example.com/repo.git",
                local_path=str(reg),
                is_default=False,
            )
        ]
    )
    manifest = Manifest(entries=[_make_entry("team", "local", "custom")])
    args = _make_args(bundle_names=["team"], yes=True)

    with patch(
        "ksm.commands.sync.pull_repo",
        side_effect=RuntimeError("network error"),
    ):
        code = run_sync(
            args,
            registry_index=idx,
            manifest=manifest,
            manifest_path=ksm_dir / "manifest.json",
            target_local=target,
            target_global=tmp_path / "global",
        )

    assert code == 0
    captured = capsys.readouterr()
    assert "warning" in captured.err.lower()


def test_sync_entry_resolve_failure_warns(
    tmp_path: Path,
    capsys: pytest.CaptureFixture[str],
) -> None:
    """_sync_entry prints warning when bundle can't be resolved."""
    from ksm.commands.sync import run_sync

    ksm_dir = tmp_path / "ksm"
    ksm_dir.mkdir()
    reg = tmp_path / "registries" / "default"
    reg.mkdir(parents=True)
    # No bundle files in registry — resolve will fail

    idx = RegistryIndex(
        registries=[
            RegistryEntry(
                name="default",
                url=None,
                local_path=str(reg),
                is_default=True,
            )
        ]
    )
    manifest = Manifest(entries=[_make_entry("missing", "local", "default")])
    args = _make_args(bundle_names=["missing"], yes=True)

    code = run_sync(
        args,
        registry_index=idx,
        manifest=manifest,
        manifest_path=ksm_dir / "manifest.json",
        target_local=tmp_path / "local",
        target_global=tmp_path / "global",
    )

    assert code == 0
    captured = capsys.readouterr()
    assert "warning" in captured.err.lower()


def test_sync_entry_install_system_exit_warns(
    tmp_path: Path,
    capsys: pytest.CaptureFixture[str],
) -> None:
    """_sync_entry prints warning when install raises SystemExit."""
    from ksm.commands.sync import run_sync

    ksm_dir = tmp_path / "ksm"
    ksm_dir.mkdir()
    reg = tmp_path / "registries" / "default"
    _setup_bundle(reg, "aws", {"skills/f.md": b"data"})

    target = tmp_path / "workspace" / ".kiro"
    target.mkdir(parents=True)

    idx = RegistryIndex(
        registries=[
            RegistryEntry(
                name="default",
                url=None,
                local_path=str(reg),
                is_default=True,
            )
        ]
    )
    manifest = Manifest(entries=[_make_entry("aws", "local", "default")])
    args = _make_args(bundle_names=["aws"], yes=True)

    with patch(
        "ksm.commands.sync.install_bundle",
        side_effect=SystemExit(1),
    ):
        code = run_sync(
            args,
            registry_index=idx,
            manifest=manifest,
            manifest_path=ksm_dir / "manifest.json",
            target_local=target,
            target_global=tmp_path / "global",
        )

    assert code == 0
    captured = capsys.readouterr()
    assert "warning" in captured.err.lower()


# --- Property-based tests ---


# Feature: kiro-settings-manager, Property 9: Sync aborts on non-y confirmation
@given(
    response=st.text(min_size=1, max_size=5).filter(lambda s: s.strip() != "y"),
)
@h_settings(suppress_health_check=[HealthCheck.function_scoped_fixture])
def test_property_sync_aborts_on_non_y(
    response: str,
    capsys: pytest.CaptureFixture[str],
) -> None:
    """Property 9: Sync aborts on non-y confirmation."""
    import tempfile

    from ksm.commands.sync import run_sync

    with tempfile.TemporaryDirectory() as td:
        base = Path(td)
        reg = base / "reg"
        _setup_bundle(reg, "b", {"skills/f.md": b"new"})
        target = base / "target" / ".kiro"
        target.mkdir(parents=True)
        (target / "skills").mkdir()
        (target / "skills" / "f.md").write_bytes(b"old")

        ksm_dir = base / "ksm"
        ksm_dir.mkdir()

        idx = RegistryIndex(
            registries=[
                RegistryEntry(
                    name="default",
                    url=None,
                    local_path=str(reg),
                    is_default=True,
                )
            ]
        )
        manifest = Manifest(entries=[_make_entry("b", files=["skills/f.md"])])

        args = _make_args(bundle_names=["b"])

        with (
            patch("builtins.input", return_value=response),
            patch("sys.stdin") as mock_stdin,
        ):
            mock_stdin.isatty.return_value = True
            code = run_sync(
                args,
                registry_index=idx,
                manifest=manifest,
                manifest_path=ksm_dir / "manifest.json",
                target_local=target,
                target_global=base / "global" / ".kiro",
            )

        assert code == 0
        assert (target / "skills" / "f.md").read_bytes() == b"old"


# Feature: kiro-settings-manager, Property 10: Sync re-copies bundle files from source
@given(
    content=st.binary(min_size=1, max_size=50),
)
def test_property_sync_recopies_from_source(
    content: bytes,
) -> None:
    """Property 10: Sync re-copies bundle files from source."""
    import tempfile

    from ksm.commands.sync import run_sync

    with tempfile.TemporaryDirectory() as td:
        base = Path(td)
        reg = base / "reg"
        _setup_bundle(reg, "b", {"skills/f.md": content})
        target = base / "target" / ".kiro"
        target.mkdir(parents=True)
        (target / "skills").mkdir()
        (target / "skills" / "f.md").write_bytes(b"stale")

        ksm_dir = base / "ksm"
        ksm_dir.mkdir()

        idx = RegistryIndex(
            registries=[
                RegistryEntry(
                    name="default",
                    url=None,
                    local_path=str(reg),
                    is_default=True,
                )
            ]
        )
        manifest = Manifest(entries=[_make_entry("b", files=["skills/f.md"])])

        args = _make_args(bundle_names=["b"], yes=True)
        run_sync(
            args,
            registry_index=idx,
            manifest=manifest,
            manifest_path=ksm_dir / "manifest.json",
            target_local=target,
            target_global=base / "global" / ".kiro",
        )

        assert (target / "skills" / "f.md").read_bytes() == content


# Feature: kiro-settings-manager, Property 11: Sync continues past unknown bundles
@given(
    unknown_name=st.from_regex(r"[a-z]{3,8}", fullmatch=True),
)
@h_settings(suppress_health_check=[HealthCheck.function_scoped_fixture])
def test_property_sync_continues_past_unknown(
    unknown_name: str,
    capsys: pytest.CaptureFixture[str],
) -> None:
    """Property 11: Sync continues past unknown bundles."""
    import tempfile

    from ksm.commands.sync import run_sync

    with tempfile.TemporaryDirectory() as td:
        base = Path(td)
        reg = base / "reg"
        _setup_bundle(reg, "known", {"skills/f.md": b"new"})
        target = base / "target" / ".kiro"
        target.mkdir(parents=True)
        (target / "skills").mkdir()
        (target / "skills" / "f.md").write_bytes(b"old")

        ksm_dir = base / "ksm"
        ksm_dir.mkdir()

        idx = RegistryIndex(
            registries=[
                RegistryEntry(
                    name="default",
                    url=None,
                    local_path=str(reg),
                    is_default=True,
                )
            ]
        )
        manifest = Manifest(
            entries=[
                _make_entry("known", files=["skills/f.md"]),
            ]
        )

        args = _make_args(bundle_names=[unknown_name, "known"], yes=True)
        code = run_sync(
            args,
            registry_index=idx,
            manifest=manifest,
            manifest_path=ksm_dir / "manifest.json",
            target_local=target,
            target_global=base / "global" / ".kiro",
        )

        assert code == 0
        captured = capsys.readouterr()
        assert unknown_name in captured.err
        assert (target / "skills" / "f.md").read_bytes() == b"new"


# Feature: kiro-settings-manager, Property 12: Sync updates manifest timestamp
@given(data=st.data())
def test_property_sync_updates_timestamp(
    data: st.DataObject,
) -> None:
    """Property 12: Sync updates manifest timestamp."""
    import tempfile

    from ksm.commands.sync import run_sync

    with tempfile.TemporaryDirectory() as td:
        base = Path(td)
        reg = base / "reg"
        _setup_bundle(reg, "b", {"skills/f.md": b"x"})
        target = base / "target" / ".kiro"
        ksm_dir = base / "ksm"
        ksm_dir.mkdir()

        idx = RegistryIndex(
            registries=[
                RegistryEntry(
                    name="default",
                    url=None,
                    local_path=str(reg),
                    is_default=True,
                )
            ]
        )
        old_ts = "2020-01-01T00:00:00+00:00"
        manifest = Manifest(entries=[_make_entry("b", files=["skills/f.md"])])
        manifest.entries[0].updated_at = old_ts

        before = datetime.now(timezone.utc)
        args = _make_args(bundle_names=["b"], yes=True)
        run_sync(
            args,
            registry_index=idx,
            manifest=manifest,
            manifest_path=ksm_dir / "manifest.json",
            target_local=target,
            target_global=base / "global" / ".kiro",
        )

        updated = datetime.fromisoformat(manifest.entries[0].updated_at)
        assert updated >= before


# --- Tests for file-level diff output (Req 22) ---


def test_sync_prints_diff_summary(
    tmp_path: Path,
    capsys: pytest.CaptureFixture[str],
) -> None:
    """Sync prints file-level diff summary after re-install."""
    from ksm.commands.sync import run_sync

    reg = tmp_path / "reg"
    _setup_bundle(reg, "aws", {"skills/f.md": b"new-content"})
    target = tmp_path / "target" / ".kiro"
    target.mkdir(parents=True)
    (target / "skills").mkdir()
    (target / "skills" / "f.md").write_bytes(b"old-content")

    ksm_dir = tmp_path / "ksm"
    ksm_dir.mkdir(parents=True)

    idx = RegistryIndex(
        registries=[
            RegistryEntry(
                name="default",
                url=None,
                local_path=str(reg),
                is_default=True,
            )
        ]
    )
    manifest = Manifest(entries=[_make_entry("aws", files=["skills/f.md"])])

    args = _make_args(bundle_names=["aws"], yes=True)
    code = run_sync(
        args,
        registry_index=idx,
        manifest=manifest,
        manifest_path=ksm_dir / "manifest.json",
        target_local=target,
        target_global=tmp_path / "global" / ".kiro",
    )

    assert code == 0
    captured = capsys.readouterr()
    # Should contain diff symbol for updated file
    assert "~" in captured.err or "updated" in captured.err.lower()


def test_sync_non_tty_without_yes_returns_error(
    tmp_path: Path,
    capsys: pytest.CaptureFixture[str],
) -> None:
    """Sync returns 1 when stdin is not a TTY and --yes not given."""
    from ksm.commands.sync import run_sync

    reg = tmp_path / "reg"
    _setup_bundle(reg, "aws", {"skills/f.md": b"data"})
    ksm_dir = tmp_path / "ksm"
    ksm_dir.mkdir()

    idx = RegistryIndex(
        registries=[
            RegistryEntry(
                name="default",
                url=None,
                local_path=str(reg),
                is_default=True,
            )
        ]
    )
    manifest = Manifest(entries=[_make_entry("aws", files=["skills/f.md"])])
    args = _make_args(bundle_names=["aws"])

    with patch("sys.stdin") as mock_stdin:
        mock_stdin.isatty.return_value = False
        code = run_sync(
            args,
            registry_index=idx,
            manifest=manifest,
            manifest_path=ksm_dir / "manifest.json",
            target_local=tmp_path / "local",
            target_global=tmp_path / "global",
        )

    assert code == 1
    captured = capsys.readouterr()
    assert "error:" in captured.err
    assert "--yes" in captured.err


def test_sync_confirmation_message_global_scope(
    tmp_path: Path,
) -> None:
    """_build_confirmation_message shows global scope per bundle."""
    from ksm.commands.sync import _build_confirmation_message

    entries = [_make_entry("aws", scope="global", files=["skills/f.md"])]
    msg = _build_confirmation_message(entries)
    assert "global" in msg


def test_sync_confirmation_message_mixed_scope(
    tmp_path: Path,
) -> None:
    """_build_confirmation_message shows scope per bundle for mixed."""
    from ksm.commands.sync import _build_confirmation_message

    entries = [
        _make_entry("aws", scope="local", files=["skills/f.md"]),
        _make_entry("team", scope="global", files=["steering/s.md"]),
    ]
    msg = _build_confirmation_message(entries)
    assert "local" in msg
    assert "global" in msg


def test_sync_dry_run_prints_preview(
    tmp_path: Path,
    capsys: pytest.CaptureFixture[str],
) -> None:
    """Sync with --dry-run prints preview without modifying files."""
    from ksm.commands.sync import run_sync

    reg = tmp_path / "reg"
    _setup_bundle(reg, "aws", {"skills/f.md": b"new"})
    target = tmp_path / "target" / ".kiro"
    target.mkdir(parents=True)
    (target / "skills").mkdir()
    (target / "skills" / "f.md").write_bytes(b"old")

    ksm_dir = tmp_path / "ksm"
    ksm_dir.mkdir()

    idx = RegistryIndex(
        registries=[
            RegistryEntry(
                name="default",
                url=None,
                local_path=str(reg),
                is_default=True,
            )
        ]
    )
    manifest = Manifest(entries=[_make_entry("aws", files=["skills/f.md"])])
    args = _make_args(bundle_names=["aws"], yes=True, dry_run=True)
    code = run_sync(
        args,
        registry_index=idx,
        manifest=manifest,
        manifest_path=ksm_dir / "manifest.json",
        target_local=target,
        target_global=tmp_path / "global",
    )

    assert code == 0
    assert (target / "skills" / "f.md").read_bytes() == b"old"
    captured = capsys.readouterr()
    assert "Sync" in captured.err


# --- Tests for stream=sys.stderr in formatter calls (Reqs 1.1-1.3, 2.1-2.3) ---


class TestSyncFormatterStreamParam:
    """Verify sync.py passes stream=sys.stderr to formatters."""

    def test_format_error_receives_stream_stderr_no_bundles(
        self,
        tmp_path: Path,
    ) -> None:
        """format_error called with stream=sys.stderr when
        no bundles specified."""
        from ksm.commands.sync import run_sync

        ksm_dir = tmp_path / "ksm"
        ksm_dir.mkdir()
        idx = RegistryIndex(registries=[])
        manifest = Manifest(entries=[])
        args = _make_args()

        with patch(
            "ksm.commands.sync.format_error",
            wraps=__import__("ksm.errors", fromlist=["format_error"]).format_error,
        ) as mock_fmt:
            run_sync(
                args,
                registry_index=idx,
                manifest=manifest,
                manifest_path=ksm_dir / "manifest.json",
                target_local=tmp_path / "local",
                target_global=tmp_path / "global",
            )

        mock_fmt.assert_called_once()
        _, kwargs = mock_fmt.call_args
        assert kwargs.get("stream") is sys.stderr

    def test_format_error_receives_stream_stderr_not_installed(
        self,
        tmp_path: Path,
    ) -> None:
        """format_error called with stream=sys.stderr when
        bundle not installed."""
        from ksm.commands.sync import run_sync

        ksm_dir = tmp_path / "ksm"
        ksm_dir.mkdir()
        idx = RegistryIndex(registries=[])
        manifest = Manifest(entries=[])
        args = _make_args(bundle_names=["nonexistent"])

        with patch(
            "ksm.commands.sync.format_error",
            wraps=__import__("ksm.errors", fromlist=["format_error"]).format_error,
        ) as mock_fmt:
            run_sync(
                args,
                registry_index=idx,
                manifest=manifest,
                manifest_path=ksm_dir / "manifest.json",
                target_local=tmp_path / "local",
                target_global=tmp_path / "global",
            )

        mock_fmt.assert_called_once()
        _, kwargs = mock_fmt.call_args
        assert kwargs.get("stream") is sys.stderr

    def test_format_error_receives_stream_stderr_tty_check(
        self,
        tmp_path: Path,
    ) -> None:
        """format_error called with stream=sys.stderr when
        stdin is not a TTY (confirmation blocked)."""
        from ksm.commands.sync import run_sync

        ksm_dir = tmp_path / "ksm"
        ksm_dir.mkdir()
        reg = tmp_path / "reg"
        _setup_bundle(reg, "aws", {"skills/f.md": b"data"})

        idx = RegistryIndex(
            registries=[
                RegistryEntry(
                    name="default",
                    url=None,
                    local_path=str(reg),
                    is_default=True,
                )
            ]
        )
        manifest = Manifest(entries=[_make_entry("aws", files=["skills/f.md"])])
        args = _make_args(bundle_names=["aws"])

        with (
            patch("sys.stdin") as mock_stdin,
            patch(
                "ksm.commands.sync.format_error",
                wraps=__import__(
                    "ksm.errors",
                    fromlist=["format_error"],
                ).format_error,
            ) as mock_fmt,
        ):
            mock_stdin.isatty.return_value = False
            run_sync(
                args,
                registry_index=idx,
                manifest=manifest,
                manifest_path=ksm_dir / "manifest.json",
                target_local=tmp_path / "local",
                target_global=tmp_path / "global",
            )

        mock_fmt.assert_called_once()
        _, kwargs = mock_fmt.call_args
        assert kwargs.get("stream") is sys.stderr

    def test_format_warning_receives_stream_stderr_pull_fail(
        self,
        tmp_path: Path,
    ) -> None:
        """format_warning called with stream=sys.stderr when
        pull_repo fails."""
        from ksm.commands.sync import run_sync

        ksm_dir = tmp_path / "ksm"
        ksm_dir.mkdir()
        reg = tmp_path / "reg"
        _setup_bundle(reg, "team", {"skills/f.md": b"data"})

        target = tmp_path / "workspace" / ".kiro"
        (target / "skills").mkdir(parents=True)
        (target / "skills" / "f.md").write_bytes(b"old")

        idx = RegistryIndex(
            registries=[
                RegistryEntry(
                    name="custom",
                    url="https://example.com/repo.git",
                    local_path=str(reg),
                    is_default=False,
                )
            ]
        )
        manifest = Manifest(
            entries=[
                _make_entry(
                    "team",
                    "local",
                    "custom",
                    files=["skills/f.md"],
                )
            ]
        )
        args = _make_args(bundle_names=["team"], yes=True)

        with (
            patch(
                "ksm.commands.sync.pull_repo",
                side_effect=RuntimeError("net err"),
            ),
            patch(
                "ksm.commands.sync.format_warning",
                wraps=__import__(
                    "ksm.errors",
                    fromlist=["format_warning"],
                ).format_warning,
            ) as mock_fmt,
        ):
            run_sync(
                args,
                registry_index=idx,
                manifest=manifest,
                manifest_path=ksm_dir / "manifest.json",
                target_local=target,
                target_global=tmp_path / "global",
            )

        assert mock_fmt.call_count >= 1
        # Check the first call (pull failure warning)
        _, kwargs = mock_fmt.call_args_list[0]
        assert kwargs.get("stream") is sys.stderr

    def test_format_warning_receives_stream_stderr_resolve_fail(
        self,
        tmp_path: Path,
    ) -> None:
        """format_warning called with stream=sys.stderr when
        bundle can't be resolved during sync."""
        from ksm.commands.sync import run_sync

        ksm_dir = tmp_path / "ksm"
        ksm_dir.mkdir()
        reg = tmp_path / "reg"
        reg.mkdir(parents=True)

        idx = RegistryIndex(
            registries=[
                RegistryEntry(
                    name="default",
                    url=None,
                    local_path=str(reg),
                    is_default=True,
                )
            ]
        )
        manifest = Manifest(
            entries=[
                _make_entry(
                    "missing",
                    "local",
                    "default",
                )
            ]
        )
        args = _make_args(bundle_names=["missing"], yes=True)

        with patch(
            "ksm.commands.sync.format_warning",
            wraps=__import__(
                "ksm.errors",
                fromlist=["format_warning"],
            ).format_warning,
        ) as mock_fmt:
            run_sync(
                args,
                registry_index=idx,
                manifest=manifest,
                manifest_path=ksm_dir / "manifest.json",
                target_local=tmp_path / "local",
                target_global=tmp_path / "global",
            )

        mock_fmt.assert_called_once()
        _, kwargs = mock_fmt.call_args
        assert kwargs.get("stream") is sys.stderr


# --- Tests for green success prefix (Req 3.3, 3.4, 3.5) ---
# Feature: color-and-scope-selection
# **Validates: Requirements 3.3, 3.4, 3.5**


class TestSyncGreenSuccessPrefix:
    """Property 14: sync success message includes
    success-styled checkmark and bundle name."""

    _SUCCESS = "\033[92m"
    _RESET = "\033[0m"

    def test_synced_prefix_success_on_tty(
        self,
        tmp_path: Path,
        capsys: pytest.CaptureFixture[str],
    ) -> None:
        """Property 14: successful sync prints success-styled
        checkmark to stderr when stream is TTY."""
        from ksm.commands.sync import run_sync

        reg = tmp_path / "reg"
        _setup_bundle(reg, "aws", {"skills/f.md": b"new"})
        target = tmp_path / "target" / ".kiro"
        target.mkdir(parents=True)
        (target / "skills").mkdir()
        (target / "skills" / "f.md").write_bytes(b"old")

        ksm_dir = tmp_path / "ksm"
        ksm_dir.mkdir(parents=True)

        idx = RegistryIndex(
            registries=[
                RegistryEntry(
                    name="default",
                    url=None,
                    local_path=str(reg),
                    is_default=True,
                )
            ]
        )
        manifest = Manifest(entries=[_make_entry("aws", files=["skills/f.md"])])
        args = _make_args(bundle_names=["aws"], yes=True)

        with patch.dict(
            "os.environ",
            {"TERM": "xterm-256color"},
            clear=True,
        ):
            with patch(
                "sys.stderr.isatty",
                return_value=True,
            ):
                code = run_sync(
                    args,
                    registry_index=idx,
                    manifest=manifest,
                    manifest_path=(ksm_dir / "manifest.json"),
                    target_local=target,
                    target_global=(tmp_path / "global" / ".kiro"),
                )

        assert code == 0
        captured = capsys.readouterr()
        assert "Synced" in captured.err
        assert self._SUCCESS in captured.err

    def test_synced_prefix_plain_with_no_color(
        self,
        tmp_path: Path,
        capsys: pytest.CaptureFixture[str],
    ) -> None:
        """Property 14: output is plain text when NO_COLOR set."""
        from ksm.commands.sync import run_sync

        reg = tmp_path / "reg"
        _setup_bundle(reg, "aws", {"skills/f.md": b"new"})
        target = tmp_path / "target" / ".kiro"
        target.mkdir(parents=True)
        (target / "skills").mkdir()
        (target / "skills" / "f.md").write_bytes(b"old")

        ksm_dir = tmp_path / "ksm"
        ksm_dir.mkdir(parents=True)

        idx = RegistryIndex(
            registries=[
                RegistryEntry(
                    name="default",
                    url=None,
                    local_path=str(reg),
                    is_default=True,
                )
            ]
        )
        manifest = Manifest(entries=[_make_entry("aws", files=["skills/f.md"])])
        args = _make_args(bundle_names=["aws"], yes=True)

        with patch.dict(
            "os.environ",
            {"NO_COLOR": "1"},
            clear=False,
        ):
            code = run_sync(
                args,
                registry_index=idx,
                manifest=manifest,
                manifest_path=(ksm_dir / "manifest.json"),
                target_local=target,
                target_global=(tmp_path / "global" / ".kiro"),
            )

        assert code == 0
        captured = capsys.readouterr()
        assert "Synced" in captured.err
        assert "\033[" not in captured.err

    def test_synced_prefix_contains_bundle_name(
        self,
        tmp_path: Path,
        capsys: pytest.CaptureFixture[str],
    ) -> None:
        """Property 14: success message includes bundle name
        alongside the green prefix.
        **Validates: Requirements 3.5**"""
        from ksm.commands.sync import run_sync

        reg = tmp_path / "reg"
        _setup_bundle(reg, "aws", {"skills/f.md": b"new"})
        target = tmp_path / "target" / ".kiro"
        target.mkdir(parents=True)
        (target / "skills").mkdir()
        (target / "skills" / "f.md").write_bytes(b"old")

        ksm_dir = tmp_path / "ksm"
        ksm_dir.mkdir(parents=True)

        idx = RegistryIndex(
            registries=[
                RegistryEntry(
                    name="default",
                    url=None,
                    local_path=str(reg),
                    is_default=True,
                )
            ]
        )
        manifest = Manifest(entries=[_make_entry("aws", files=["skills/f.md"])])
        args = _make_args(bundle_names=["aws"], yes=True)

        code = run_sync(
            args,
            registry_index=idx,
            manifest=manifest,
            manifest_path=(ksm_dir / "manifest.json"),
            target_local=target,
            target_global=(tmp_path / "global" / ".kiro"),
        )

        assert code == 0
        captured = capsys.readouterr()
        assert "Synced" in captured.err
        assert "aws" in captured.err


# --- Tests for colored sync confirmation prompt (Reqs 10.1-10.3) ---
# Feature: color-and-scope-selection
# **Validates: Requirements 10.1, 10.2, 10.3**


class FakeTTY:
    """A fake stream that reports isatty() = True."""

    def isatty(self) -> bool:
        return True


class TestSyncConfirmationColor:
    """Properties 17 & 18: sync confirmation wraps bundle
    names in bold and scope description in bold."""

    _BOLD = "\033[1m"
    _ACCENT = "\033[96m"
    _RESET = "\033[0m"

    # --- Property 17: bundle names wrapped in bold ---

    @given(
        bundle_name=st.from_regex(r"[a-z]{3,10}", fullmatch=True),
    )
    @h_settings(suppress_health_check=[HealthCheck.function_scoped_fixture])
    def test_property_sync_confirm_bold_bundle_tty(
        self,
        bundle_name: str,
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        """Property 17: sync confirmation wraps bundle names
        in bold when stream is a TTY.
        **Validates: Requirements 10.1**"""
        from ksm.commands.sync import (
            _build_confirmation_message,
        )

        monkeypatch.delenv("NO_COLOR", raising=False)
        monkeypatch.setenv("TERM", "xterm-256color")

        entries = [
            _make_entry(
                bundle_name,
                scope="local",
                files=["skills/f.md"],
            )
        ]

        stream = FakeTTY()
        msg = _build_confirmation_message(entries, stream=stream)

        expected = f"{self._ACCENT}{bundle_name}{self._RESET}"
        assert expected in msg

    # --- Property 18: scope shown per bundle in muted ---

    @given(
        scope_combo=st.sampled_from(["local_only", "global_only", "mixed"]),
    )
    @h_settings(suppress_health_check=[HealthCheck.function_scoped_fixture])
    def test_property_sync_confirm_bold_scope_tty(
        self,
        scope_combo: str,
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        """Property 18: sync confirmation shows scope per bundle."""
        from ksm.commands.sync import (
            _build_confirmation_message,
        )

        monkeypatch.delenv("NO_COLOR", raising=False)
        monkeypatch.setenv("TERM", "xterm-256color")

        if scope_combo == "local_only":
            entries = [_make_entry("aws", scope="local")]
        elif scope_combo == "global_only":
            entries = [_make_entry("aws", scope="global")]
        else:
            entries = [
                _make_entry("aws", scope="local"),
                _make_entry("team", scope="global"),
            ]

        stream = FakeTTY()
        msg = _build_confirmation_message(entries, stream=stream)

        for e in entries:
            assert e.scope in msg

    # --- Plain text when stream is None ---

    def test_sync_confirm_plain_when_stream_none(
        self,
    ) -> None:
        """sync confirmation is plain text when stream is None."""
        from ksm.commands.sync import (
            _build_confirmation_message,
        )

        entries = [
            _make_entry(
                "aws",
                scope="local",
                files=["skills/f.md"],
            )
        ]

        msg = _build_confirmation_message(entries, stream=None)

        assert "\033[" not in msg
        assert "aws" in msg
        assert "local" in msg

    # --- Plain text when stream is non-TTY ---

    def test_sync_confirm_plain_when_non_tty(
        self,
    ) -> None:
        """sync confirmation is plain text when stream is non-TTY."""
        import io

        from ksm.commands.sync import (
            _build_confirmation_message,
        )

        entries = [
            _make_entry(
                "aws",
                scope="global",
                files=["steering/s.md"],
            )
        ]

        stream = io.StringIO()
        msg = _build_confirmation_message(entries, stream=stream)

        assert "\033[" not in msg
        assert "aws" in msg
        assert "global" in msg

    # --- Preserves existing structure ---

    def test_sync_confirm_preserves_structure(
        self,
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        """Colored confirmation preserves prompt structure."""
        from ksm.commands.sync import (
            _build_confirmation_message,
        )

        monkeypatch.delenv("NO_COLOR", raising=False)
        monkeypatch.setenv("TERM", "xterm-256color")

        entries = [
            _make_entry(
                "aws",
                scope="local",
                files=["skills/f.md", "steering/s.md"],
            ),
            _make_entry(
                "team",
                scope="global",
                files=["skills/g.md"],
            ),
        ]

        stream = FakeTTY()
        msg = _build_confirmation_message(entries, stream=stream)

        assert "2 bundles" in msg
        assert "Continue?" in msg
        assert "[y/n]" in msg
