"""Tests for sync command and related functions."""

from pathlib import Path
from unittest.mock import AsyncMock, patch

import pytest

from compose_farm import executor as executor_module
from compose_farm import state as state_module
from compose_farm.cli import management as cli_management_module
from compose_farm.config import Config, Host
from compose_farm.executor import CommandResult, check_stack_running


@pytest.fixture
def mock_config(tmp_path: Path) -> Config:
    """Create a mock config for testing."""
    compose_dir = tmp_path / "stacks"
    compose_dir.mkdir()

    # Create stack directories with compose files
    for stack in ["plex", "jellyfin", "grafana"]:
        stack_dir = compose_dir / stack
        stack_dir.mkdir()
        (stack_dir / "compose.yaml").write_text(f"# {stack} compose file\n")

    return Config(
        compose_dir=compose_dir,
        hosts={
            "nas01": Host(address="192.168.1.10", user="admin", port=22),
            "nas02": Host(address="192.168.1.11", user="admin", port=22),
        },
        stacks={
            "plex": "nas01",
            "jellyfin": "nas01",
            "grafana": "nas02",
        },
    )


@pytest.fixture
def state_dir(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
    """Create a temporary state directory and patch _get_state_path."""
    state_path = tmp_path / ".config" / "compose-farm"
    state_path.mkdir(parents=True)

    def mock_get_state_path() -> Path:
        return state_path / "state.yaml"

    monkeypatch.setattr(state_module, "_get_state_path", mock_get_state_path)
    return state_path


class TestCheckStackRunning:
    """Tests for check_stack_running function."""

    @pytest.mark.asyncio
    async def test_stack_running(self, mock_config: Config) -> None:
        """Returns True when stack has running containers."""
        with patch.object(executor_module, "run_command", new_callable=AsyncMock) as mock_run:
            mock_run.return_value = CommandResult(
                stack="plex",
                exit_code=0,
                success=True,
                stdout="abc123\ndef456\n",
            )
            result = await check_stack_running(mock_config, "plex", "nas01")
            assert result is True

    @pytest.mark.asyncio
    async def test_stack_not_running(self, mock_config: Config) -> None:
        """Returns False when stack has no running containers."""
        with patch.object(executor_module, "run_command", new_callable=AsyncMock) as mock_run:
            mock_run.return_value = CommandResult(
                stack="plex",
                exit_code=0,
                success=True,
                stdout="",
            )
            result = await check_stack_running(mock_config, "plex", "nas01")
            assert result is False

    @pytest.mark.asyncio
    async def test_command_failed(self, mock_config: Config) -> None:
        """Returns False when command fails."""
        with patch.object(executor_module, "run_command", new_callable=AsyncMock) as mock_run:
            mock_run.return_value = CommandResult(
                stack="plex",
                exit_code=1,
                success=False,
            )
            result = await check_stack_running(mock_config, "plex", "nas01")
            assert result is False


class TestMergeState:
    """Tests for _merge_state helper function."""

    def test_merge_adds_new_stacks(self) -> None:
        """Merging adds newly discovered stacks to existing state."""
        current: dict[str, str | list[str]] = {"plex": "nas01"}
        discovered: dict[str, str | list[str]] = {"jellyfin": "nas02"}
        removed: list[str] = []

        result = cli_management_module._merge_state(current, discovered, removed)

        assert result == {"plex": "nas01", "jellyfin": "nas02"}

    def test_merge_updates_existing_stacks(self) -> None:
        """Merging updates stacks that changed hosts."""
        current: dict[str, str | list[str]] = {"plex": "nas01", "jellyfin": "nas01"}
        discovered: dict[str, str | list[str]] = {"plex": "nas02"}  # plex moved to nas02
        removed: list[str] = []

        result = cli_management_module._merge_state(current, discovered, removed)

        assert result == {"plex": "nas02", "jellyfin": "nas01"}

    def test_merge_removes_stopped_stacks(self) -> None:
        """Merging removes stacks that were checked but not found."""
        current: dict[str, str | list[str]] = {
            "plex": "nas01",
            "jellyfin": "nas01",
            "grafana": "nas02",
        }
        discovered: dict[str, str | list[str]] = {"plex": "nas01"}  # only plex still running
        removed = ["jellyfin"]  # jellyfin was checked and not found

        result = cli_management_module._merge_state(current, discovered, removed)

        # jellyfin removed, grafana untouched (wasn't in the refresh scope)
        assert result == {"plex": "nas01", "grafana": "nas02"}

    def test_merge_preserves_unrelated_stacks(self) -> None:
        """Merging preserves stacks that weren't part of the refresh."""
        current: dict[str, str | list[str]] = {
            "plex": "nas01",
            "jellyfin": "nas01",
            "grafana": "nas02",
        }
        discovered: dict[str, str | list[str]] = {"plex": "nas02"}  # only refreshed plex
        removed: list[str] = []  # nothing was removed

        result = cli_management_module._merge_state(current, discovered, removed)

        # plex updated, others preserved
        assert result == {"plex": "nas02", "jellyfin": "nas01", "grafana": "nas02"}


class TestReportSyncChanges:
    """Tests for _report_sync_changes function."""

    def test_reports_added(self, capsys: pytest.CaptureFixture[str]) -> None:
        """Reports newly discovered stacks."""
        cli_management_module._report_sync_changes(
            added=["plex", "jellyfin"],
            removed=[],
            changed=[],
            discovered={"plex": "nas01", "jellyfin": "nas02"},
            current_state={},
        )
        captured = capsys.readouterr()
        assert "New stacks found (2)" in captured.out
        assert "+ plex on nas01" in captured.out
        assert "+ jellyfin on nas02" in captured.out

    def test_reports_removed(self, capsys: pytest.CaptureFixture[str]) -> None:
        """Reports stacks that are no longer running."""
        cli_management_module._report_sync_changes(
            added=[],
            removed=["grafana"],
            changed=[],
            discovered={},
            current_state={"grafana": "nas01"},
        )
        captured = capsys.readouterr()
        assert "Stacks no longer running (1)" in captured.out
        assert "- grafana (was on nas01)" in captured.out

    def test_reports_changed(self, capsys: pytest.CaptureFixture[str]) -> None:
        """Reports stacks that moved to a different host."""
        cli_management_module._report_sync_changes(
            added=[],
            removed=[],
            changed=[("plex", "nas01", "nas02")],
            discovered={"plex": "nas02"},
            current_state={"plex": "nas01"},
        )
        captured = capsys.readouterr()
        assert "Stacks on different hosts (1)" in captured.out
        assert "~ plex: nas01 → nas02" in captured.out


class TestRefreshCommand:
    """Tests for the refresh command with stack arguments."""

    def test_refresh_specific_stack_partial_merge(
        self, mock_config: Config, capsys: pytest.CaptureFixture[str]
    ) -> None:
        """Refreshing specific stacks merges with existing state."""
        # Mock existing state
        existing_state = {"plex": "nas01", "jellyfin": "nas01", "grafana": "nas02"}

        with (
            patch(
                "compose_farm.cli.management.get_stacks",
                return_value=(["plex"], mock_config),
            ),
            patch(
                "compose_farm.cli.management.load_state",
                return_value=existing_state,
            ),
            patch(
                "compose_farm.cli.management._discover_stacks",
                return_value={"plex": "nas02"},  # plex moved to nas02
            ),
            patch("compose_farm.cli.management._snapshot_stacks"),
            patch("compose_farm.cli.management.save_state") as mock_save,
        ):
            # stacks=["plex"], all_stacks=False -> partial refresh
            cli_management_module.refresh(
                stacks=["plex"],
                all_stacks=False,
                config=None,
                log_path=None,
                dry_run=False,
            )

            # Should have merged: plex updated, others preserved
            mock_save.assert_called_once()
            saved_state = mock_save.call_args[0][1]
            assert saved_state == {"plex": "nas02", "jellyfin": "nas01", "grafana": "nas02"}

    def test_refresh_all_replaces_state(
        self, mock_config: Config, capsys: pytest.CaptureFixture[str]
    ) -> None:
        """Refreshing all stacks replaces the entire state."""
        existing_state = {"plex": "nas01", "jellyfin": "nas01", "old-service": "nas02"}

        with (
            patch(
                "compose_farm.cli.management.get_stacks",
                return_value=(["plex", "jellyfin", "grafana"], mock_config),
            ),
            patch(
                "compose_farm.cli.management.load_state",
                return_value=existing_state,
            ),
            patch(
                "compose_farm.cli.management._discover_stacks",
                return_value={"plex": "nas01", "grafana": "nas02"},  # jellyfin not running
            ),
            patch("compose_farm.cli.management._snapshot_stacks"),
            patch("compose_farm.cli.management.save_state") as mock_save,
        ):
            # stacks=None, all_stacks=False -> defaults to all (full refresh)
            cli_management_module.refresh(
                stacks=None,
                all_stacks=False,
                config=None,
                log_path=None,
                dry_run=False,
            )

            # Should have replaced: only discovered stacks remain
            mock_save.assert_called_once()
            saved_state = mock_save.call_args[0][1]
            assert saved_state == {"plex": "nas01", "grafana": "nas02"}

    def test_refresh_with_all_flag_full_refresh(self, mock_config: Config) -> None:
        """Using --all flag forces full refresh even with stack names."""
        existing_state = {"plex": "nas01", "jellyfin": "nas01"}

        with (
            patch(
                "compose_farm.cli.management.get_stacks",
                return_value=(["plex", "jellyfin", "grafana"], mock_config),
            ),
            patch(
                "compose_farm.cli.management.load_state",
                return_value=existing_state,
            ),
            patch(
                "compose_farm.cli.management._discover_stacks",
                return_value={"plex": "nas01"},  # only plex running
            ),
            patch("compose_farm.cli.management._snapshot_stacks"),
            patch("compose_farm.cli.management.save_state") as mock_save,
        ):
            # all_stacks=True -> full refresh (replaces state)
            cli_management_module.refresh(
                stacks=["plex"],  # ignored when --all is set
                all_stacks=True,
                config=None,
                log_path=None,
                dry_run=False,
            )

            mock_save.assert_called_once()
            saved_state = mock_save.call_args[0][1]
            # Full refresh: only discovered stacks
            assert saved_state == {"plex": "nas01"}

    def test_refresh_partial_removes_stopped_stack(self, mock_config: Config) -> None:
        """Partial refresh removes a stack if it was checked but not found."""
        existing_state = {"plex": "nas01", "jellyfin": "nas01", "grafana": "nas02"}

        with (
            patch(
                "compose_farm.cli.management.get_stacks",
                return_value=(["plex", "jellyfin"], mock_config),
            ),
            patch(
                "compose_farm.cli.management.load_state",
                return_value=existing_state,
            ),
            patch(
                "compose_farm.cli.management._discover_stacks",
                return_value={"plex": "nas01"},  # jellyfin not running
            ),
            patch("compose_farm.cli.management._snapshot_stacks"),
            patch("compose_farm.cli.management.save_state") as mock_save,
        ):
            cli_management_module.refresh(
                stacks=["plex", "jellyfin"],
                all_stacks=False,
                config=None,
                log_path=None,
                dry_run=False,
            )

            mock_save.assert_called_once()
            saved_state = mock_save.call_args[0][1]
            # jellyfin removed (was checked), grafana preserved (wasn't checked)
            assert saved_state == {"plex": "nas01", "grafana": "nas02"}

    def test_refresh_dry_run_no_state_change(
        self, mock_config: Config, capsys: pytest.CaptureFixture[str]
    ) -> None:
        """Dry run shows changes but doesn't modify state."""
        existing_state = {"plex": "nas01"}

        with (
            patch(
                "compose_farm.cli.management.get_stacks",
                return_value=(["plex"], mock_config),
            ),
            patch(
                "compose_farm.cli.management.load_state",
                return_value=existing_state,
            ),
            patch(
                "compose_farm.cli.management._discover_stacks",
                return_value={"plex": "nas02"},  # would change
            ),
            patch("compose_farm.cli.management.save_state") as mock_save,
        ):
            cli_management_module.refresh(
                stacks=["plex"],
                all_stacks=False,
                config=None,
                log_path=None,
                dry_run=True,
            )

            # Should not save state in dry run
            mock_save.assert_not_called()

            captured = capsys.readouterr()
            assert "dry-run" in captured.out
