"""Tests for specter_cli.sync — project root detection and rsync."""

from __future__ import annotations

from unittest.mock import patch

import pytest

from specter_cli.sync import (
    find_project_root,
    load_specterignore,
    rsync_workspace,
)


class TestFindProjectRoot:
    def test_finds_git_dir(self, tmp_path):
        (tmp_path / ".git").mkdir()
        sub = tmp_path / "src" / "pkg"
        sub.mkdir(parents=True)
        assert find_project_root(sub) == tmp_path

    def test_finds_pyproject_toml(self, tmp_path):
        (tmp_path / "pyproject.toml").touch()
        sub = tmp_path / "deep" / "nested"
        sub.mkdir(parents=True)
        assert find_project_root(sub) == tmp_path

    def test_finds_setup_py(self, tmp_path):
        (tmp_path / "setup.py").touch()
        assert find_project_root(tmp_path) == tmp_path

    def test_finds_nearest_marker(self, tmp_path):
        """Nearest marker wins when multiple markers exist."""
        (tmp_path / ".git").mkdir()
        sub = tmp_path / "subproject"
        sub.mkdir()
        (sub / "pyproject.toml").touch()
        assert find_project_root(sub) == sub

    def test_fallback_to_start(self, tmp_path):
        """No marker found — returns the start directory."""
        empty = tmp_path / "no_markers"
        empty.mkdir()
        result = find_project_root(empty)
        assert result == empty

    def test_spectertoml_marker(self, tmp_path):
        (tmp_path / ".specter.toml").touch()
        assert find_project_root(tmp_path) == tmp_path

    def test_makefile_marker(self, tmp_path):
        (tmp_path / "Makefile").touch()
        assert find_project_root(tmp_path) == tmp_path


class TestLoadSpecterignore:
    def test_no_file_returns_empty(self, tmp_path):
        assert load_specterignore(tmp_path) == []

    def test_reads_patterns(self, tmp_path):
        ignore = tmp_path / ".specterignore"
        ignore.write_text("*.log\ndata/\n# comment\n\nweights/\n")
        patterns = load_specterignore(tmp_path)
        assert patterns == ["*.log", "data/", "weights/"]

    def test_strips_whitespace(self, tmp_path):
        ignore = tmp_path / ".specterignore"
        ignore.write_text("  *.log  \n  data/  \n")
        patterns = load_specterignore(tmp_path)
        assert patterns == ["*.log", "data/"]


class TestRsyncWorkspace:
    @patch("specter_cli.sync.subprocess.run")
    def test_basic_rsync_call(self, mock_run, tmp_path):
        mock_run.return_value.returncode = 0

        rsync_workspace(
            project_root=tmp_path,
            rsync_ssh="ssh -o ControlMaster=auto",
            remote_dest="root@10.0.0.1",
            remote_workspace="~/.specter/workspace",
        )

        mock_run.assert_called_once()
        args = mock_run.call_args[0][0]

        assert args[0] == "rsync"
        assert "-az" in args
        assert "--delete" in args
        assert "-e" in args
        assert "ssh -o ControlMaster=auto" in args

        # Source should end with /
        assert args[-2].endswith("/")
        # Dest should be user@host:workspace/
        assert args[-1] == "root@10.0.0.1:~/.specter/workspace/"

    @patch("specter_cli.sync.subprocess.run")
    def test_default_excludes_included(self, mock_run, tmp_path):
        mock_run.return_value.returncode = 0

        rsync_workspace(
            project_root=tmp_path,
            rsync_ssh="ssh",
            remote_dest="root@10.0.0.1",
        )

        args = mock_run.call_args[0][0]
        for exclude in [".git", "__pycache__", ".venv"]:
            assert "--exclude" in args
            # Find all excludes.
            excludes_in_args = [args[i + 1] for i in range(len(args)) if args[i] == "--exclude"]
            assert exclude in excludes_in_args

    @patch("specter_cli.sync.subprocess.run")
    def test_extra_excludes(self, mock_run, tmp_path):
        mock_run.return_value.returncode = 0

        rsync_workspace(
            project_root=tmp_path,
            rsync_ssh="ssh",
            remote_dest="root@10.0.0.1",
            extra_excludes=["*.bin", "checkpoints/"],
        )

        args = mock_run.call_args[0][0]
        excludes = [args[i + 1] for i in range(len(args)) if args[i] == "--exclude"]
        assert "*.bin" in excludes
        assert "checkpoints/" in excludes

    @patch("specter_cli.sync.subprocess.run")
    def test_no_delete(self, mock_run, tmp_path):
        mock_run.return_value.returncode = 0

        rsync_workspace(
            project_root=tmp_path,
            rsync_ssh="ssh",
            remote_dest="root@10.0.0.1",
            delete=False,
        )

        args = mock_run.call_args[0][0]
        assert "--delete" not in args

    @patch("specter_cli.sync.subprocess.run")
    def test_rsync_failure_raises(self, mock_run, tmp_path):
        import subprocess

        mock_run.return_value.returncode = 1
        mock_run.return_value.stderr = "rsync error"
        mock_run.return_value.stdout = ""

        with pytest.raises(subprocess.CalledProcessError):
            rsync_workspace(
                project_root=tmp_path,
                rsync_ssh="ssh",
                remote_dest="root@10.0.0.1",
            )

    @patch("specter_cli.sync.subprocess.run")
    def test_specterignore_patterns_included(self, mock_run, tmp_path):
        mock_run.return_value.returncode = 0
        ignore = tmp_path / ".specterignore"
        ignore.write_text("*.log\ndata/\n")

        rsync_workspace(
            project_root=tmp_path,
            rsync_ssh="ssh",
            remote_dest="root@10.0.0.1",
        )

        args = mock_run.call_args[0][0]
        excludes = [args[i + 1] for i in range(len(args)) if args[i] == "--exclude"]
        assert "*.log" in excludes
        assert "data/" in excludes
